@madgex/design-system-ce 6.0.3 → 7.0.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.
@@ -10,36 +10,48 @@
10
10
  @keydown.esc="handleKeyEsc"
11
11
  @keydown.enter="handleKeyDownEnter"
12
12
  >
13
- <input
14
- :id="comboboxid"
15
- ref="$comboInput"
16
- :value="searchValue"
17
- class="mds-form-control"
18
- autocomplete="off"
19
- type="text"
20
- role="combobox"
21
- :name="name"
22
- :placeholder="placeholder"
23
- :aria-controls="listBoxId"
24
- :aria-expanded="ariaExpanded"
25
- aria-autocomplete="list"
26
- :aria-describedby="describedbyId"
27
- :aria-activedescendant="getOptionIdByIndex(selectedIndex)"
28
- :aria-invalid="ariaInvalid"
29
- @input="handleInput"
30
- @change="handleChange"
31
- @blur="handleBlur"
32
- @focus="handleFocus"
33
- />
34
- <ComboboxClear v-if="searchValue.length > 0" @clear="handleClear" />
35
- <ListBox :id="listBoxId" :hidden="!expanded" :aria-labelledby="`${comboboxid}-label`" :is-loading="isLoading">
13
+ <!-- .mds-form-control class used to imitate an input in this wrapper, while having pills/search input inside it -->
14
+ <div class="mds-form-control" @click="handleClickInputWrapper">
15
+ <span class="mds-visually-hidden" v-if="multiple && vModel?.length">{{ i18nText.selectedOptionsLabel }}</span>
16
+ <ul class="mds-combobox__pills" v-if="multiple && vModel?.length">
17
+ <li v-for="option in vModel" :key="getOptionValue(option)">
18
+ <ComboboxPill :aria-label-remove="getPillAriaLabel(option)" @remove="handleClickPill(option)">
19
+ {{ getOptionLabel(option) }}
20
+ </ComboboxPill>
21
+ </li>
22
+ </ul>
23
+ <!-- no name so search input is not included in the <form> -->
24
+ <input
25
+ :id="comboboxId"
26
+ class="mds-combobox__search-input"
27
+ ref="$comboInput"
28
+ :value="searchTextVModel"
29
+ autocomplete="off"
30
+ type="text"
31
+ role="combobox"
32
+ :placeholder="placeholder"
33
+ :aria-controls="listBoxId"
34
+ :aria-expanded="ariaExpanded"
35
+ aria-autocomplete="list"
36
+ :aria-describedby="describedbyId"
37
+ :aria-activedescendant="getOptionIdByIndex(selectedIndex)"
38
+ :aria-invalid="ariaInvalid"
39
+ @input="handleInput"
40
+ @change="handleChange"
41
+ @blur="handleBlur"
42
+ @focus="handleFocus"
43
+ />
44
+ <ComboboxClear v-if="searchTextVModel.length > 0" @clear="handleClear" />
45
+ </div>
46
+
47
+ <ListBox :id="listBoxId" :hidden="!expanded" :aria-labelledby="`${comboboxId}-label`" :is-loading="isLoading">
36
48
  <ListBoxOption
37
49
  v-for="(option, index) in visibleOptions"
38
50
  :id="getOptionIdByIndex(index)"
39
51
  :key="index"
40
52
  :option-label="getOptionLabel(option)"
41
53
  :focused="selectedIndex === index"
42
- :search-value="searchValue"
54
+ :search-text="searchTextVModel"
43
55
  @mousedown="handleMouseDownOption(option)"
44
56
  />
45
57
  </ListBox>
@@ -50,13 +62,11 @@
50
62
  :results-message_plural="i18nText.resultsMessage_plural"
51
63
  />
52
64
  <!-- No default <slot/> used, so fallback child content is destroyed on mount -->
53
- <!-- target-inputs <slot/> so we can easily find inputs to populate with option selection -->
54
- <span ref="$targetInputs"><slot name="target-inputs"></slot></span>
55
65
  </div>
56
66
  </template>
57
67
 
58
68
  <script setup>
59
- import { computed, provide, ref, useTemplateRef } from 'vue';
69
+ import { computed, provide, ref, useHost, useTemplateRef, watch } from 'vue';
60
70
  import safeGet from 'just-safe-get';
61
71
  import { useDebounceFn } from '@vueuse/core';
62
72
  import Bourne from '@hapi/bourne';
@@ -64,16 +74,16 @@ import ComboboxClear from './ComboboxClear.vue';
64
74
  import ListBox from './ListBox.vue';
65
75
  import ListBoxOption from './ListBoxOption.vue';
66
76
  import ComboboxAriaLive from './ComboboxAriaLive.vue';
77
+ import ComboboxPill from './ComboboxPill.vue';
67
78
 
68
79
  /*
69
80
  * as this is a Web Component, all props are string-ish types, hence why `options` is JSON parsed, see `parsedPropOptions`.
70
81
  * https://vuejs.org/guide/extras/web-components.html#props
71
82
  */
72
83
  const props = defineProps({
73
- comboboxid: { type: String, required: true },
84
+ comboboxId: { type: String, required: true },
74
85
  placeholder: { type: String, default: '' },
75
- name: { type: [String, Boolean], default: false },
76
- value: { type: String, default: '' },
86
+ name: { type: String, required: true },
77
87
  options: { type: String, default: '[]' },
78
88
  iconpath: { type: String, default: '/assets/icons.svg' },
79
89
  dataAriaInvalid: { type: String, default: '' },
@@ -88,74 +98,128 @@ const props = defineProps({
88
98
  apiOptionsPath: { type: String, default: undefined },
89
99
  /** where to grab the visual label from the option object e.g. 'label' or 'title' or 'nested.object.label' */
90
100
  optionLabelPath: { type: String, default: 'label' },
101
+ /** where to grab the value from the option object e.g. 'value' or 'score' or 'nested.object.value' */
102
+ optionValuePath: { type: String, default: 'value' },
103
+ multiple: { type: Boolean, default: false },
104
+ });
105
+
106
+ // host is the element in the DOM. We assume we are always a Vue CustomElement, so host should always exist
107
+ const host = useHost();
108
+ // combobox reports its value to the nearest `<form>` element https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals
109
+ // see ../../custom-elements/mds-combobox.js on supporting code making this work
110
+ const internals = host?.attachInternals();
111
+
112
+ /**
113
+ * `value` is defined with `defineModel` instead of `defineProps` so we can update its value.
114
+ *
115
+ * > Note: this defineModel + vModel + watch combination allows us to synchronise the HTML attribute, auto-parse the string to JSON value and setFormValue.
116
+ * > Recommended to keep this pattern for `value` attribute syncing/parsing.
117
+ * > Other patterns were tried but failed to achieve all of these goals and would cause subtle bugs.
118
+ * */
119
+ const vModelRawString = defineModel('value', { type: String, default: '' });
120
+ /** synchronise vModel to attribute */
121
+ watch(vModelRawString, (val) => host?.setAttribute('value', val), { immediate: true });
122
+ /** @type {import('vue').WritableComputedRef<object|Array<object>} parsed raw string of vModelRawString, this is the interface to use in our code */
123
+ const vModel = computed({
124
+ get() {
125
+ const defaultValue = props.multiple ? [] : null;
126
+ return Bourne.safeParse(vModelRawString.value) || defaultValue;
127
+ },
128
+ set(val) {
129
+ if (!val) vModelRawString.value = '';
130
+ else vModelRawString.value = JSON.stringify(val);
131
+ },
91
132
  });
133
+ /** synchronise vModel with formValue - enables parent `<form>` to obtain this data */
134
+ watch(
135
+ vModel,
136
+ (val) => {
137
+ let formValue;
138
+ if (props.multiple) {
139
+ // if multiple, we can use `FormData` to populate multiple values of the same name
140
+ formValue = new FormData();
141
+ for (const option of val || []) {
142
+ const optVal = getOptionValue(option);
143
+ if (optVal) formValue.append(props.name, optVal);
144
+ }
145
+ } else {
146
+ formValue = getOptionValue(val);
147
+ }
148
+ internals?.setFormValue(formValue || '');
149
+ },
150
+ { immediate: true },
151
+ );
152
+
153
+ /** `search-text` is defined with `defineModel` instead of `defineProps` for greater control */
154
+ const searchTextVModel = defineModel('search-text', {
155
+ type: String,
156
+ default: '',
157
+ get(val) {
158
+ return val;
159
+ },
160
+ set(val) {
161
+ // use attribute as source of truth
162
+ host?.setAttribute('search-text', val);
163
+ return val;
164
+ },
165
+ });
166
+ // initial prefill searchText if we are not multiple and vModel is set
167
+ if (!props.multiple && vModel.value && !searchTextVModel.value) {
168
+ searchTextVModel.value = getOptionLabel(vModel.value);
169
+ }
92
170
 
93
171
  const $comboInput = useTemplateRef('$comboInput');
94
- const $targetInputs = useTemplateRef('$targetInputs');
95
172
 
96
173
  const expanded = ref(false);
97
174
  /** `selectedIndex` aka "highlighted option", set by using keyboard controls */
98
175
  const selectedIndex = ref(null);
99
- const searchValue = ref(props.value);
176
+
100
177
  const isLoading = ref(false);
101
178
  /** used if apiUrl is set, otherwise `parsedPropOptions` is used */
102
179
  const apiOptions = ref([]);
103
180
 
104
181
  // as props must be string-ish types, we parse the `options` into a real array
105
- const parsedPropOptions = computed(() => {
106
- try {
107
- return Bourne.parse(props.options);
108
- } catch (e) {
109
- console.error(e);
110
- return [];
111
- }
112
- });
182
+ const parsedPropOptions = computed(() => Bourne.safeParse(props.options) || []);
113
183
 
114
184
  const i18nText = computed(() => {
115
- return props.i18n
116
- ? JSON.parse(props.i18n)
117
- : {
118
- loadingText: 'Loading',
119
- resultsMessage: '{count} result available',
120
- resultsMessage_plural: '{count} results available',
121
- clearInput: 'clear input',
122
- };
185
+ const defaults = {
186
+ loadingText: 'Loading',
187
+ resultsMessage: '{count} result available',
188
+ resultsMessage_plural: '{count} results available',
189
+ clearInput: 'clear input',
190
+ removePill: 'Remove {label}',
191
+ selectedOptionsLabel: 'Selected options:',
192
+ };
193
+ const overrides = props.i18n ? Bourne.safeParse(props.i18n) : {};
194
+ return { ...defaults, ...overrides };
123
195
  });
124
-
196
+ /** filtered props.options or apiOptions, by searchTextVModel */
125
197
  const visibleOptions = computed(() => {
126
- if (!props.apiUrl) {
127
- return parsedPropOptions.value.filter((opt) =>
128
- getOptionLabel(opt).toLowerCase().includes(searchValue.value.toLowerCase()),
129
- );
130
- }
198
+ if (props.apiUrl) return apiOptions.value;
131
199
 
132
- return apiOptions.value;
133
- });
134
- const listBoxId = computed(() => {
135
- return `${props.comboboxid}-listbox`;
200
+ return parsedPropOptions.value.filter((opt) =>
201
+ getOptionLabel(opt).toLowerCase().includes(searchTextVModel.value.toLowerCase()),
202
+ );
136
203
  });
204
+ const listBoxId = computed(() => `${props.comboboxId}-listbox`);
137
205
 
138
206
  /** generate an DOM `id` for a option, based on index number */
139
207
  function getOptionIdByIndex(index) {
140
208
  if (typeof index === 'number' && index > -1) {
141
- return `${props.comboboxid}-option-${index}`;
209
+ return `${props.comboboxId}-option-${index}`;
142
210
  }
143
211
  return undefined;
144
212
  }
145
213
 
146
- const ariaExpanded = computed(() => {
147
- // These must be strings to apply as an aria attribute of the same name
148
- return expanded.value ? 'true' : 'false';
149
- });
150
-
151
- const ariaInvalid = computed(() => {
152
- return props.dataAriaInvalid ? 'true' : 'false';
153
- });
214
+ /** aria-expanded must be string to apply as an aria attribute */
215
+ const ariaExpanded = computed(() => (expanded.value ? 'true' : 'false'));
216
+ /** aria-invalid must be string to apply as an aria attribute */
217
+ const ariaInvalid = computed(() => (props.dataAriaInvalid ? 'true' : 'false'));
154
218
 
155
219
  /**
156
220
  * When user chooses an option:
157
221
  * - set search input to chosen option's label
158
- * - set any hidden target input values based on option
222
+ * - set value
159
223
  * - close list menu
160
224
  * - reset selectedIndex
161
225
  * @param newOption
@@ -166,35 +230,29 @@ function chooseOption(newOption) {
166
230
  console.error('attempted to choose an option, but option was falsy');
167
231
  return;
168
232
  }
169
- searchValue.value = getOptionLabel(newOption);
170
- setTargetValues(newOption);
233
+ // set value and searchText
234
+ if (!props.multiple) {
235
+ vModel.value = newOption;
236
+ searchTextVModel.value = getOptionLabel(newOption);
237
+ } else {
238
+ const existingOption = vModel.value?.find((_option) => getOptionValue(_option) === getOptionValue(newOption));
239
+ if (!existingOption) vModel.value = [...vModel.value, newOption];
240
+ searchTextVModel.value = '';
241
+ }
171
242
  makeInactive();
172
243
  selectedIndex.value = null;
173
244
  }
174
245
 
175
- /**
176
- * Update target inputs with value from an option.
177
- * If option is not supplied, target input values will be cleared
178
- * @param {object?} option
179
- */
180
- function setTargetValues(option) {
181
- const targetInputs = Array.from($targetInputs.value?.querySelectorAll('[data-key]'));
182
- for (const el of targetInputs) {
183
- // if no option, clear target value
184
- el.value = option ? safeGet(option, el.getAttribute('data-key')) : '';
185
- // ensure external code like htmx reacts to the new value
186
- el.dispatchEvent(new Event('input', { bubbles: true }));
187
- }
188
- }
189
246
  function makeActive() {
190
247
  expanded.value = true;
191
248
  }
192
249
  function makeInactive() {
193
250
  expanded.value = false;
194
251
  }
252
+ /** clear search text input, if not multiple mode, also clear vModel value */
195
253
  function clearField() {
196
- searchValue.value = '';
197
- setTargetValues();
254
+ searchTextVModel.value = '';
255
+ if (!props.multiple) vModel.value = null;
198
256
  }
199
257
 
200
258
  /**
@@ -203,7 +261,7 @@ function clearField() {
203
261
  */
204
262
  const debouncedFetchApiOptions = useDebounceFn(async function fetchApiOptions() {
205
263
  if (!props.apiUrl) return;
206
- const searchQuery = searchValue?.value?.trim();
264
+ const searchQuery = searchTextVModel?.value?.trim();
207
265
  try {
208
266
  isLoading.value = true;
209
267
  let res = await fetch(`${props.apiUrl}?${props.apiQueryKey}=${encodeURIComponent(searchQuery)}`);
@@ -229,22 +287,36 @@ function getOptionLabel(option) {
229
287
  const label = safeGet(option, props.optionLabelPath);
230
288
  return String(label);
231
289
  }
290
+ /**
291
+ * where do we grab the value from the option object?
292
+ * option.value or option['nested.object.value.path']
293
+ * @param {object} option
294
+ * @returns {string} value
295
+ */
296
+ function getOptionValue(option) {
297
+ const val = safeGet(option, props.optionValuePath);
298
+ return String(val ?? '');
299
+ }
300
+
301
+ function getPillAriaLabel(option) {
302
+ return i18nText.value.removePill.replace('{label}', getOptionLabel(option));
303
+ }
232
304
 
233
305
  /**
234
306
  * - reset selectedIndex
235
- * - copies the existing value into `searchValue` (because we manually handle input/change events)
307
+ * - copies the existing value into `searchTextVModel` (because we manually handle input/change events)
236
308
  * - fetch from api if applicable
237
309
  * @param event input event
238
310
  */
239
311
  function handleInput(event) {
240
312
  selectedIndex.value = null;
241
- searchValue.value = event.target ? event.target.value : '';
313
+ searchTextVModel.value = event.target ? event.target.value : '';
242
314
  handleChange();
243
315
  debouncedFetchApiOptions();
244
316
  }
245
317
  function handleChange() {
246
- if (searchValue.value.length === 0) clearField();
247
- if (searchValue.value.length >= props.minSearchCharacters) {
318
+ if (searchTextVModel.value.length === 0) clearField();
319
+ if (searchTextVModel.value.length >= props.minSearchCharacters) {
248
320
  makeActive();
249
321
  } else {
250
322
  makeInactive();
@@ -257,6 +329,12 @@ function handleClear() {
257
329
  clearField();
258
330
  $comboInput.value?.focus();
259
331
  }
332
+ function handleClickInputWrapper() {
333
+ $comboInput.value?.focus();
334
+ }
335
+ function handleClickPill(option) {
336
+ vModel.value = vModel.value.filter((opt) => getOptionValue(opt) !== getOptionValue(option));
337
+ }
260
338
  /**
261
339
  * As the `Enter` key is handled seperately (see `handleKeyDownEnter`),
262
340
  * we need this handler for mouse clicks on an option
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <div class="mds-combobox__pill mds-border">
3
+ <span class="mds-combobox__pill-text"><slot /></span>
4
+ <button class="mds-combobox__pill-icon" type="button" :aria-label="ariaLabelRemove" @click="handleClickPill">
5
+ <svg aria-hidden="true" focusable="false" class="mds-icon mds-icon--close mds-icon--sm">
6
+ <use :href="`${iconPath}#icon-close`" />
7
+ </svg>
8
+ </button>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup>
13
+ import { inject } from 'vue';
14
+
15
+ defineProps({
16
+ ariaLabelRemove: { type: String, required: true },
17
+ });
18
+ const emit = defineEmits(['remove']);
19
+ const iconPath = inject('iconPath');
20
+ function handleClickPill() {
21
+ emit('remove');
22
+ }
23
+ </script>
@@ -5,7 +5,6 @@
5
5
  role="option"
6
6
  :class="{ 'mds-combobox__option--focused': focused }"
7
7
  :aria-selected="focused.toString()"
8
- @mousedown="$emit('mousedown', $event)"
9
8
  v-html="highlightOption()"
10
9
  />
11
10
  </template>
@@ -16,7 +15,7 @@ import { useTemplateRef, watch } from 'vue';
16
15
  const props = defineProps({
17
16
  optionLabel: { type: String, required: true },
18
17
  focused: { type: Boolean, default: false },
19
- searchValue: { type: String, default: '' },
18
+ searchText: { type: String, default: '' },
20
19
  });
21
20
 
22
21
  const $listItem = useTemplateRef('$listItem');
@@ -31,7 +30,7 @@ watch(
31
30
  );
32
31
  function highlightOption() {
33
32
  const optionLabelHtml = props.optionLabel.replace(
34
- new RegExp(props.searchValue, 'gi'),
33
+ new RegExp(props.searchText, 'gi'),
35
34
  (match) => `<span class="mds-combobox__option--marked">${match}</span>`,
36
35
  );
37
36
  return optionLabelHtml;
@@ -20,6 +20,7 @@ export default {
20
20
  customMenuButtons: this.customMenuButtons,
21
21
  i18nText: this.i18nText,
22
22
  ariaDescribedBy: this.describedbyId,
23
+ noMenu: this.noMenu,
23
24
  };
24
25
  },
25
26
  props: {
@@ -47,6 +48,10 @@ export default {
47
48
  type: String,
48
49
  default: '',
49
50
  },
51
+ noMenu: {
52
+ type: Boolean,
53
+ default: false,
54
+ },
50
55
  },
51
56
  data() {
52
57
  return {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="mds-text-editor__menu" role="toolbar" :aria-label="i18nText.toolbarLabel" :aria-controls="id">
2
+ <div class="mds-text-editor__menu" role="toolbar" v-if="!noMenu" :aria-label="i18nText.toolbarLabel" :aria-controls="id">
3
3
  <TextEditorButton
4
4
  v-for="button in menuButtons"
5
5
  :id="button.id"
@@ -35,7 +35,7 @@ export default {
35
35
  // TextEditorPopoverLink,
36
36
  },
37
37
 
38
- inject: ['id', 'customMenuButtons', 'i18nText'],
38
+ inject: ['id', 'customMenuButtons', 'i18nText', 'noMenu'],
39
39
 
40
40
  props: {
41
41
  editor: {
@@ -4,5 +4,8 @@ import { defineCustomElement } from 'vue';
4
4
  import Combobox from '../components/combobox/Combobox.ce.vue';
5
5
 
6
6
  const MdsCombobox = defineCustomElement(Combobox, { shadowRoot: false });
7
+ // we must set `formAssociated` manually as combobox uses elementInternals and currently vue does not support this
8
+ // https://github.com/vuejs/core/issues/10948#issuecomment-2877323232
9
+ Object.defineProperty(MdsCombobox, 'formAssociated', { value: true, writable: false });
7
10
 
8
11
  export default MdsCombobox;
@@ -1 +1 @@
1
- import{v as A,x as S,a as v,i as d,f as $,u as x,q as h,b as U,y as V,t as q,z as P,A as N,e as H,r as y,B as b,C as L,p as K,j as R,D as ie,F as ue,l as ce,s as de}from"../runtime-dom.esm-bundler.js";function pe(a){return a&&a.__esModule&&Object.prototype.hasOwnProperty.call(a,"default")?a.default:a}var M=fe;function fe(a,e,s){if(!a)return s;var t,n;if(Array.isArray(e)&&(t=e.slice(0)),typeof e=="string"&&(t=e.split(".")),typeof e=="symbol"&&(t=[e]),!Array.isArray(t))throw new Error("props arg must be an array, a string or a symbol");for(;t.length;)if(n=t.shift(),!a||(a=a[n],a===void 0))return s;return a}typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const j=()=>{};function me(a,e){function s(...t){return new Promise((n,o)=>{Promise.resolve(a(()=>e.apply(this,t),{fn:e,thisArg:this,args:t})).then(n).catch(o)})}return s}function ve(a,e={}){let s,t,n=j;const o=c=>{clearTimeout(c),n(),n=j};let l;return c=>{const w=A(a),f=A(e.maxWait);return s&&o(s),w<=0||f!==void 0&&f<=0?(t&&(o(t),t=void 0),Promise.resolve(c())):new Promise((p,I)=>{n=e.rejectOnCancel?I:p,l=c,f&&!t&&(t=setTimeout(()=>{s&&o(s),t=void 0,p(l())},f)),s=setTimeout(()=>{t&&o(t),t=void 0,p(c())},w)})}}function W(a,e=200,s={}){return me(ve(e,s),a)}var k={},D;function he(){return D||(D=1,(function(a){const e={suspectRx:/"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*\:/};a.parse=function(s,...t){const n=typeof t[0]=="object"&&t[0],o=t.length>1||!n?t[0]:void 0,l=t.length>1&&t[1]||n||{},i=JSON.parse(s,o);return l.protoAction==="ignore"||!i||typeof i!="object"||!s.match(e.suspectRx)||a.scan(i,l),i},a.scan=function(s,t={}){let n=[s];for(;n.length;){const o=n;n=[];for(const l of o){if(Object.prototype.hasOwnProperty.call(l,"__proto__")){if(t.protoAction!=="remove")throw new SyntaxError("Object contains forbidden prototype property");delete l.__proto__}for(const i in l){const c=l[i];c&&typeof c=="object"&&n.push(l[i])}}}},a.safeParse=function(s,t){try{return a.parse(s,t)}catch{return null}}})(k)),k}var be=he();const ye=pe(be),ge=["aria-label","title"],_e={"aria-hidden":"true",focusable:"false",class:"mds-icon mds-icon--close mds-icon--sm"},xe=["href"],$e={__name:"ComboboxClear",setup(a){const e=S("iconPath"),s=S("clearInput");return(t,n)=>(d(),v("button",{class:"mds-combobox__clear mds-button mds-button--plain",type:"button",onClick:n[0]||(n[0]=o=>t.$emit("clear",o)),onKeydown:n[1]||(n[1]=h(o=>t.$emit("clear",o),["enter"])),"aria-label":x(s),title:x(s)},[(d(),v("svg",_e,[$("use",{href:`${x(e)}#icon-close`},null,8,xe)]))],40,ge))}},we=["hidden"],Ie={key:0,class:"mds-combobox-loading"},Se={"aria-hidden":"true",focusable:"true",class:"mds-icon mds-icon--spinner mds-icon--after"},Ce=["href"],Oe={class:"mds-visually-hidden"},Le={__name:"ListBox",props:{hidden:{type:Boolean,default:!0},isLoading:{type:Boolean,default:!0}},setup(a){const e=S("iconPath"),s=S("loadingText");return(t,n)=>(d(),v("ul",{class:"mds-combobox__listbox",role:"listbox",hidden:a.hidden},[a.isLoading?(d(),v("li",Ie,[(d(),v("svg",Se,[$("use",{href:`${x(e)}#icon-spinner`},null,8,Ce)])),$("span",Oe,q(x(s)),1)])):U("",!0),V(t.$slots,"default")],8,we))}},Me=["aria-selected","innerHTML"],ke={__name:"ListBoxOption",props:{optionLabel:{type:String,required:!0},focused:{type:Boolean,default:!1},searchValue:{type:String,default:""}},setup(a){const e=a,s=P("$listItem");N(()=>e.focused,n=>{n&&s.value?.scrollIntoView({block:"nearest",inline:"nearest"})});function t(){return e.optionLabel.replace(new RegExp(e.searchValue,"gi"),o=>`<span class="mds-combobox__option--marked">${o}</span>`)}return(n,o)=>(d(),v("li",{ref_key:"$listItem",ref:s,class:H(["mds-combobox__option",{"mds-combobox__option--focused":a.focused}]),role:"option","aria-selected":a.focused.toString(),onMousedown:o[0]||(o[0]=l=>n.$emit("mousedown",l)),innerHTML:t()},null,42,Me))}},Pe={role:"status",class:"mds-visually-hidden"},Te={__name:"ComboboxAriaLive",props:{visibleOptions:{type:Array,default:()=>[]},expanded:{type:Boolean,default:!1},resultsMessage:{type:String},resultsMessage_plural:{type:String}},setup(a){const e=a;N([()=>e.expanded,()=>e.visibleOptions],()=>{n()},{deep:!0});const s=y(null),t=W(function(){const l=e.visibleOptions.length===1?e.resultsMessage:e.resultsMessage_plural;s.value=l.replace("{count}",e.visibleOptions.length)},1400);function n(){s.value=null,e.expanded&&t()}return(o,l)=>(d(),v("div",Pe,q(s.value),1))}},Be=["id","value","name","placeholder","aria-controls","aria-expanded","aria-describedby","aria-activedescendant","aria-invalid"],Fe={__name:"Combobox.ce",props:{comboboxid:{type:String,required:!0},placeholder:{type:String,default:""},name:{type:[String,Boolean],default:!1},value:{type:String,default:""},options:{type:String,default:"[]"},iconpath:{type:String,default:"/assets/icons.svg"},dataAriaInvalid:{type:String,default:""},i18n:{type:String,default:""},describedbyId:{type:String,default:""},minSearchCharacters:{type:Number,default:2},apiUrl:{type:String,default:void 0},apiQueryKey:{type:String,default:"searchText"},apiOptionsPath:{type:String,default:void 0},optionLabelPath:{type:String,default:"label"}},setup(a){const e=a,s=P("$comboInput"),t=P("$targetInputs"),n=y(!1),o=y(null),l=y(e.value),i=y(!1),c=y([]),w=b(()=>{try{return ye.parse(e.options)}catch(r){return console.error(r),[]}}),f=b(()=>e.i18n?JSON.parse(e.i18n):{loadingText:"Loading",resultsMessage:"{count} result available",resultsMessage_plural:"{count} results available",clearInput:"clear input"}),p=b(()=>e.apiUrl?c.value:w.value.filter(r=>C(r).toLowerCase().includes(l.value.toLowerCase()))),I=b(()=>`${e.comboboxid}-listbox`);function T(r){if(typeof r=="number"&&r>-1)return`${e.comboboxid}-option-${r}`}const G=b(()=>n.value?"true":"false"),Q=b(()=>e.dataAriaInvalid?"true":"false");function B(r){if(!r){console.error("attempted to choose an option, but option was falsy");return}l.value=C(r),F(r),g(),o.value=null}function F(r){const m=Array.from(t.value?.querySelectorAll("[data-key]"));for(const u of m)u.value=r?M(r,u.getAttribute("data-key")):"",u.dispatchEvent(new Event("input",{bubbles:!0}))}function z(){n.value=!0}function g(){n.value=!1}function E(){l.value="",F()}const J=W(async function(){if(!e.apiUrl)return;const m=l?.value?.trim();try{i.value=!0;let u=await fetch(`${e.apiUrl}?${e.apiQueryKey}=${encodeURIComponent(m)}`);if(!u.ok)return;u=await u.json();const _=e.apiOptionsPath?M(u,e.apiOptionsPath):u;c.value=_||[]}finally{i.value=!1}},350);function C(r){const m=M(r,e.optionLabelPath);return String(m)}function X(r){o.value=null,l.value=r.target?r.target.value:"",O(),J()}function O(){l.value.length===0&&E(),l.value.length>=e.minSearchCharacters?z():g()}function Y(){O()}function Z(){E(),s.value?.focus()}function ee(r){B(r)}function te(r){if(n.value){r.preventDefault();const m=p.value?.[o.value];m?B(m):g()}}function ne(){g()}function ae(){n.value&&(o.value!==null?o.value=Math.min(o.value+1,p.value.length-1):o.value=0)}function oe(){n.value&&(o.value!==null?o.value=Math.max(o.value-1,0):o.value=p.value.length-1)}function se(){n.value&&(o.value=0)}function le(){n.value&&(o.value=p.value.length-1)}function re(){g()}return L("iconPath",e.iconpath),L("loadingText",f.value.loadingText),L("clearInput",f.value.clearInput),(r,m)=>(d(),v("div",{class:H(["mds-combobox",{"mds-combobox--active":n.value}]),onKeydown:[h(ae,["down"]),h(oe,["up"]),h(se,["home"]),h(le,["end"]),h(re,["esc"]),h(te,["enter"])]},[$("input",{id:a.comboboxid,ref_key:"$comboInput",ref:s,value:l.value,class:"mds-form-control",autocomplete:"off",type:"text",role:"combobox",name:a.name,placeholder:a.placeholder,"aria-controls":I.value,"aria-expanded":G.value,"aria-autocomplete":"list","aria-describedby":a.describedbyId,"aria-activedescendant":T(o.value),"aria-invalid":Q.value,onInput:X,onChange:O,onBlur:ne,onFocus:Y},null,40,Be),l.value.length>0?(d(),K($e,{key:0,onClear:Z})):U("",!0),R(Le,{id:I.value,hidden:!n.value,"aria-labelledby":`${a.comboboxid}-label`,"is-loading":i.value},{default:ie(()=>[(d(!0),v(ue,null,ce(p.value,(u,_)=>(d(),K(ke,{id:T(_),key:_,"option-label":C(u),focused:o.value===_,"search-value":l.value,onMousedown:Ee=>ee(u)},null,8,["id","option-label","focused","search-value","onMousedown"]))),128))]),_:1},8,["id","hidden","aria-labelledby","is-loading"]),R(Te,{"visible-options":p.value,expanded:n.value,"results-message":f.value.resultsMessage,"results-message_plural":f.value.resultsMessage_plural},null,8,["visible-options","expanded","results-message","results-message_plural"]),$("span",{ref_key:"$targetInputs",ref:t},[V(r.$slots,"target-inputs")],512)],34))}},Ke=de(Fe,{shadowRoot:!1});export{Ke as default};
1
+ import{v as H,x as w,a as f,b as m,f as b,u as C,q as g,t as L,i as M,y as X,z as Y,A as k,e as Z,r as O,B as _e,C as N,D as B,F as U,l as W,p as G,k as R,E as Q,G as ge,H as _,I as xe,s as Se}from"../runtime-dom.esm-bundler.js";function $e(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var A=Ie;function Ie(t,e,o){if(!t)return o;var a,l;if(Array.isArray(e)&&(a=e.slice(0)),typeof e=="string"&&(a=e.split(".")),typeof e=="symbol"&&(a=[e]),!Array.isArray(a))throw new Error("props arg must be an array, a string or a symbol");for(;a.length;)if(l=a.shift(),!t||(t=t[l],t===void 0))return o;return t}typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope;const z=()=>{};function Ce(t,e){function o(...a){return new Promise((l,s)=>{Promise.resolve(t(()=>e.apply(this,a),{fn:e,thisArg:this,args:a})).then(l).catch(s)})}return o}function Pe(t,e={}){let o,a,l=z;const s=r=>{clearTimeout(r),l(),l=z};let i;return r=>{const c=H(t),h=H(e.maxWait);return o&&s(o),c<=0||h!==void 0&&h<=0?(a&&(s(a),a=void 0),Promise.resolve(r())):new Promise((x,T)=>{l=e.rejectOnCancel?T:x,i=r,h&&!a&&(a=setTimeout(()=>{o&&s(o),a=void 0,x(i())},h)),o=setTimeout(()=>{a&&s(a),a=void 0,x(r())},c)})}}function ee(t,e=200,o={}){return Ce(Pe(e,o),t)}var E={},J;function Oe(){return J||(J=1,(function(t){const e={suspectRx:/"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*\:/};t.parse=function(o,...a){const l=typeof a[0]=="object"&&a[0],s=a.length>1||!l?a[0]:void 0,i=a.length>1&&a[1]||l||{},p=JSON.parse(o,s);return i.protoAction==="ignore"||!p||typeof p!="object"||!o.match(e.suspectRx)||t.scan(p,i),p},t.scan=function(o,a={}){let l=[o];for(;l.length;){const s=l;l=[];for(const i of s){if(Object.prototype.hasOwnProperty.call(i,"__proto__")){if(a.protoAction!=="remove")throw new SyntaxError("Object contains forbidden prototype property");delete i.__proto__}for(const p in i){const r=i[p];r&&typeof r=="object"&&l.push(i[p])}}}},t.safeParse=function(o,a){try{return t.parse(o,a)}catch{return null}}})(E)),E}var we=Oe();const V=$e(we),Me=["aria-label","title"],Le={"aria-hidden":"true",focusable:"false",class:"mds-icon mds-icon--close mds-icon--sm"},ke=["href"],Te={__name:"ComboboxClear",setup(t){const e=w("iconPath"),o=w("clearInput");return(a,l)=>(f(),m("button",{class:"mds-combobox__clear mds-button mds-button--plain",type:"button",onClick:l[0]||(l[0]=s=>a.$emit("clear",s)),onKeydown:l[1]||(l[1]=g(s=>a.$emit("clear",s),["enter"])),"aria-label":C(o),title:C(o)},[(f(),m("svg",Le,[b("use",{href:`${C(e)}#icon-close`},null,8,ke)]))],40,Me))}},Fe=["hidden"],Be={key:0,class:"mds-combobox-loading"},Re={"aria-hidden":"true",focusable:"true",class:"mds-icon mds-icon--spinner mds-icon--after"},Ae=["href"],Ee={class:"mds-visually-hidden"},Ve={__name:"ListBox",props:{hidden:{type:Boolean,default:!0},isLoading:{type:Boolean,default:!0}},setup(t){const e=w("iconPath"),o=w("loadingText");return(a,l)=>(f(),m("ul",{class:"mds-combobox__listbox",role:"listbox",hidden:t.hidden},[t.isLoading?(f(),m("li",Be,[(f(),m("svg",Re,[b("use",{href:`${C(e)}#icon-spinner`},null,8,Ae)])),b("span",Ee,L(C(o)),1)])):M("",!0),X(a.$slots,"default")],8,Fe))}},Ke=["aria-selected","innerHTML"],De={__name:"ListBoxOption",props:{optionLabel:{type:String,required:!0},focused:{type:Boolean,default:!1},searchText:{type:String,default:""}},setup(t){const e=t,o=Y("$listItem");k(()=>e.focused,l=>{l&&o.value?.scrollIntoView({block:"nearest",inline:"nearest"})});function a(){return e.optionLabel.replace(new RegExp(e.searchText,"gi"),s=>`<span class="mds-combobox__option--marked">${s}</span>`)}return(l,s)=>(f(),m("li",{ref_key:"$listItem",ref:o,class:Z(["mds-combobox__option",{"mds-combobox__option--focused":t.focused}]),role:"option","aria-selected":t.focused.toString(),innerHTML:a()},null,10,Ke))}},je={role:"status",class:"mds-visually-hidden"},qe={__name:"ComboboxAriaLive",props:{visibleOptions:{type:Array,default:()=>[]},expanded:{type:Boolean,default:!1},resultsMessage:{type:String},resultsMessage_plural:{type:String}},setup(t){const e=t;k([()=>e.expanded,()=>e.visibleOptions],()=>{l()},{deep:!0});const o=O(null),a=ee(function(){const i=e.visibleOptions.length===1?e.resultsMessage:e.resultsMessage_plural;o.value=i.replace("{count}",e.visibleOptions.length)},1400);function l(){o.value=null,e.expanded&&a()}return(s,i)=>(f(),m("div",je,L(o.value),1))}},He={class:"mds-combobox__pill mds-border"},Ne={class:"mds-combobox__pill-text"},Ue=["aria-label"],We={"aria-hidden":"true",focusable:"false",class:"mds-icon mds-icon--close mds-icon--sm"},Ge=["href"],Qe={__name:"ComboboxPill",props:{ariaLabelRemove:{type:String,required:!0}},emits:["remove"],setup(t,{emit:e}){const o=e,a=w("iconPath");function l(){o("remove")}return(s,i)=>(f(),m("div",He,[b("span",Ne,[X(s.$slots,"default")]),b("button",{class:"mds-combobox__pill-icon",type:"button","aria-label":t.ariaLabelRemove,onClick:l},[(f(),m("svg",We,[b("use",{href:`${C(a)}#icon-close`},null,8,Ge)]))],8,Ue)]))}},ze={key:0,class:"mds-visually-hidden"},Je={key:1,class:"mds-combobox__pills"},Xe=["id","value","placeholder","aria-controls","aria-expanded","aria-describedby","aria-activedescendant","aria-invalid"],Ye={__name:"Combobox.ce",props:ge({comboboxId:{type:String,required:!0},placeholder:{type:String,default:""},name:{type:String,required:!0},options:{type:String,default:"[]"},iconpath:{type:String,default:"/assets/icons.svg"},dataAriaInvalid:{type:String,default:""},i18n:{type:String,default:""},describedbyId:{type:String,default:""},minSearchCharacters:{type:Number,default:2},apiUrl:{type:String,default:void 0},apiQueryKey:{type:String,default:"searchText"},apiOptionsPath:{type:String,default:void 0},optionLabelPath:{type:String,default:"label"},optionValuePath:{type:String,default:"value"},multiple:{type:Boolean,default:!1}},{value:{type:String,default:""},valueModifiers:{},"search-text":{type:String,default:""},"search-textModifiers":{}}),emits:["update:value","update:search-text"],setup(t){const e=t,o=_e(),a=o?.attachInternals(),l=N(t,"value");k(l,n=>o?.setAttribute("value",n),{immediate:!0});const s=_({get(){const n=e.multiple?[]:null;return V.safeParse(l.value)||n},set(n){n?l.value=JSON.stringify(n):l.value=""}});k(s,n=>{let u;if(e.multiple){u=new FormData;for(const d of n||[]){const v=y(d);v&&u.append(e.name,v)}}else u=y(n);a?.setFormValue(u||"")},{immediate:!0});const i=N(t,"search-text",{get(n){return n},set(n){return o?.setAttribute("search-text",n),n}});!e.multiple&&s.value&&!i.value&&(i.value=I(s.value));const p=Y("$comboInput"),r=O(!1),c=O(null),h=O(!1),x=O([]),T=_(()=>V.safeParse(e.options)||[]),S=_(()=>{const n={loadingText:"Loading",resultsMessage:"{count} result available",resultsMessage_plural:"{count} results available",clearInput:"clear input",removePill:"Remove {label}",selectedOptionsLabel:"Selected options:"},u=e.i18n?V.safeParse(e.i18n):{};return{...n,...u}}),$=_(()=>e.apiUrl?x.value:T.value.filter(n=>I(n).toLowerCase().includes(i.value.toLowerCase()))),K=_(()=>`${e.comboboxId}-listbox`);function D(n){if(typeof n=="number"&&n>-1)return`${e.comboboxId}-option-${n}`}const te=_(()=>r.value?"true":"false"),ne=_(()=>e.dataAriaInvalid?"true":"false");function j(n){if(!n){console.error("attempted to choose an option, but option was falsy");return}e.multiple?(s.value?.find(d=>y(d)===y(n))||(s.value=[...s.value,n]),i.value=""):(s.value=n,i.value=I(n)),P(),c.value=null}function ae(){r.value=!0}function P(){r.value=!1}function q(){i.value="",e.multiple||(s.value=null)}const oe=ee(async function(){if(!e.apiUrl)return;const u=i?.value?.trim();try{h.value=!0;let d=await fetch(`${e.apiUrl}?${e.apiQueryKey}=${encodeURIComponent(u)}`);if(!d.ok)return;d=await d.json();const v=e.apiOptionsPath?A(d,e.apiOptionsPath):d;x.value=v||[]}finally{h.value=!1}},350);function I(n){const u=A(n,e.optionLabelPath);return String(u)}function y(n){const u=A(n,e.optionValuePath);return String(u??"")}function le(n){return S.value.removePill.replace("{label}",I(n))}function se(n){c.value=null,i.value=n.target?n.target.value:"",F(),oe()}function F(){i.value.length===0&&q(),i.value.length>=e.minSearchCharacters?ae():P()}function ie(){F()}function re(){q(),p.value?.focus()}function ue(){p.value?.focus()}function ce(n){s.value=s.value.filter(u=>y(u)!==y(n))}function de(n){j(n)}function fe(n){if(r.value){n.preventDefault();const u=$.value?.[c.value];u?j(u):P()}}function pe(){P()}function me(){r.value&&(c.value!==null?c.value=Math.min(c.value+1,$.value.length-1):c.value=0)}function ve(){r.value&&(c.value!==null?c.value=Math.max(c.value-1,0):c.value=$.value.length-1)}function he(){r.value&&(c.value=0)}function be(){r.value&&(c.value=$.value.length-1)}function ye(){P()}return B("iconPath",e.iconpath),B("loadingText",S.value.loadingText),B("clearInput",S.value.clearInput),(n,u)=>(f(),m("div",{class:Z(["mds-combobox",{"mds-combobox--active":r.value}]),onKeydown:[g(me,["down"]),g(ve,["up"]),g(he,["home"]),g(be,["end"]),g(ye,["esc"]),g(fe,["enter"])]},[b("div",{class:"mds-form-control",onClick:ue},[t.multiple&&s.value?.length?(f(),m("span",ze,L(S.value.selectedOptionsLabel),1)):M("",!0),t.multiple&&s.value?.length?(f(),m("ul",Je,[(f(!0),m(U,null,W(s.value,d=>(f(),m("li",{key:y(d)},[R(Qe,{"aria-label-remove":le(d),onRemove:v=>ce(d)},{default:Q(()=>[xe(L(I(d)),1)]),_:2},1032,["aria-label-remove","onRemove"])]))),128))])):M("",!0),b("input",{id:t.comboboxId,class:"mds-combobox__search-input",ref_key:"$comboInput",ref:p,value:i.value,autocomplete:"off",type:"text",role:"combobox",placeholder:t.placeholder,"aria-controls":K.value,"aria-expanded":te.value,"aria-autocomplete":"list","aria-describedby":t.describedbyId,"aria-activedescendant":D(c.value),"aria-invalid":ne.value,onInput:se,onChange:F,onBlur:pe,onFocus:ie},null,40,Xe),i.value.length>0?(f(),G(Te,{key:2,onClear:re})):M("",!0)]),R(Ve,{id:K.value,hidden:!r.value,"aria-labelledby":`${t.comboboxId}-label`,"is-loading":h.value},{default:Q(()=>[(f(!0),m(U,null,W($.value,(d,v)=>(f(),G(De,{id:D(v),key:v,"option-label":I(d),focused:c.value===v,"search-text":i.value,onMousedown:et=>de(d)},null,8,["id","option-label","focused","search-text","onMousedown"]))),128))]),_:1},8,["id","hidden","aria-labelledby","is-loading"]),R(qe,{"visible-options":$.value,expanded:r.value,"results-message":S.value.resultsMessage,"results-message_plural":S.value.resultsMessage_plural},null,8,["visible-options","expanded","results-message","results-message_plural"])],34))}},Ze=Se(Ye,{shadowRoot:!1});Object.defineProperty(Ze,"formAssociated",{value:!0,writable:!1});export{Ze as default};