@bagelink/vue 0.0.439 → 0.0.447

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.
Files changed (55) hide show
  1. package/dist/components/AccordionItem.vue.d.ts.map +1 -1
  2. package/dist/components/DataPreview.vue.d.ts +6 -0
  3. package/dist/components/DataPreview.vue.d.ts.map +1 -1
  4. package/dist/components/Flag.vue.d.ts +16 -0
  5. package/dist/components/Flag.vue.d.ts.map +1 -0
  6. package/dist/components/Modal.vue.d.ts.map +1 -1
  7. package/dist/components/TableSchema.vue.d.ts +6 -5
  8. package/dist/components/TableSchema.vue.d.ts.map +1 -1
  9. package/dist/components/form/inputs/FileUpload.vue.d.ts +166 -2
  10. package/dist/components/form/inputs/FileUpload.vue.d.ts.map +1 -1
  11. package/dist/components/form/inputs/TelInput.vue.d.ts +165 -0
  12. package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -0
  13. package/dist/components/form/inputs/TextInput.vue.d.ts +2 -5
  14. package/dist/components/form/inputs/TextInput.vue.d.ts.map +1 -1
  15. package/dist/components/form/inputs/index.d.ts +1 -0
  16. package/dist/components/form/inputs/index.d.ts.map +1 -1
  17. package/dist/components/index.d.ts +1 -0
  18. package/dist/components/index.d.ts.map +1 -1
  19. package/dist/components/layout/Tabs.vue.d.ts.map +1 -1
  20. package/dist/index.cjs +6513 -1544
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.mjs +6513 -1544
  24. package/dist/style.css +1877 -93
  25. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  26. package/dist/utils/allCountries.d.ts +11 -0
  27. package/dist/utils/allCountries.d.ts.map +1 -0
  28. package/package.json +3 -4
  29. package/src/components/AccordionItem.vue +8 -5
  30. package/src/components/DataPreview.vue +14 -4
  31. package/src/components/Flag.vue +1349 -0
  32. package/src/components/Modal.vue +26 -9
  33. package/src/components/ModalForm.vue +1 -1
  34. package/src/components/PageTitle.vue +8 -1
  35. package/src/components/TableSchema.vue +34 -2
  36. package/src/components/form/inputs/FileUpload.vue +74 -61
  37. package/src/components/form/inputs/SelectInput.vue +6 -0
  38. package/src/components/form/inputs/TelInput.vue +401 -0
  39. package/src/components/form/inputs/TextInput.vue +16 -53
  40. package/src/components/form/inputs/index.ts +1 -0
  41. package/src/components/index.ts +1 -0
  42. package/src/components/layout/Tabs.vue +1 -0
  43. package/src/index.ts +1 -0
  44. package/src/styles/appearance.css +610 -0
  45. package/src/styles/buttons.css +8 -0
  46. package/src/styles/layout.css +46 -1
  47. package/src/styles/mobilLayout.css +37 -0
  48. package/src/styles/modal.css +5 -2
  49. package/src/styles/text.css +39 -0
  50. package/src/utils/BagelFormUtils.ts +11 -1
  51. package/src/utils/allCountries.ts +337 -0
  52. package/src/components/whatsapp/form/MsgTemplate.vue +0 -224
  53. package/src/components/whatsapp/form/TextVariableExamples.vue +0 -79
  54. package/src/components/whatsapp/index.ts +0 -2
  55. package/src/components/whatsapp/interfaces.ts +0 -58
@@ -0,0 +1,401 @@
1
+ <template>
2
+ <div class="bagel-input" :class="{ disabled: disabled }">
3
+ <label>
4
+ {{ label }}
5
+ <div
6
+ dir="ltr"
7
+ class="flex gap-05 tel-input"
8
+ tabindex="-1"
9
+ aria-label="Country Code Selector"
10
+ aria-haspopup="listbox"
11
+ :aria-expanded="open"
12
+ @keydown.esc="reset"
13
+ @keydown.tab="reset"
14
+ >
15
+ <Dropdown :placement="'bottom'" @hide="open = false">
16
+ <span class="flex gap-05" @click="open = true">
17
+ <Icon :icon="open ? 'collapse_all' : 'expand_all'" />
18
+ <Flag
19
+ v-if="dropdownOptions.showFlags && activeCountryCode"
20
+ :country-code="activeCountryCode?.toLowerCase()"
21
+ />
22
+ </span>
23
+ <template #popper="{ hide }">
24
+ <div class="p-075 tel-countryp-dropdown">
25
+ <TextInput
26
+ v-if="searchable"
27
+ aria-label="Search by country name or country code"
28
+ placeholder="Search"
29
+ icon="search"
30
+ v-model="searchQuery"
31
+ />
32
+ <ul
33
+ ref="listEl"
34
+ class="overflow-y p-0"
35
+ :style="{ 'max-height': '400px' }"
36
+ :class="dropdownOpenDirection"
37
+ role="listbox"
38
+ >
39
+ <li
40
+ v-for="(pb, index) in sortedCountries"
41
+ role="option"
42
+ class="flex gap-075"
43
+ :key="pb.iso2 + isPreferred(pb)"
44
+ tabindex="-1"
45
+ @click="
46
+ () => {
47
+ chooseCountry(pb.iso2);
48
+ hide();
49
+ }
50
+ "
51
+ @mousemove="selectedIndex = index"
52
+ :aria-selected="activeCountryCode === pb.iso2 && !isPreferred(pb)"
53
+ >
54
+ <Flag v-if="dropdownOptions.showFlags" :country-code="pb.iso2.toLowerCase()" />
55
+ <p class="tel-country">{{ pb.name }}</p>
56
+ <span v-if="dropdownOptions.showDialCodeInList"> +{{ pb.dialCode }} </span>
57
+ </li>
58
+ </ul>
59
+ </div>
60
+ </template>
61
+ </Dropdown>
62
+ <input
63
+ :required="required"
64
+ :placeholder="placeholder"
65
+ :disabled="disabled"
66
+ v-model="phone"
67
+ type="tel"
68
+ ref="refInput"
69
+ :autocomplete="autocomplete"
70
+ :class="['vti__input']"
71
+ :id="id"
72
+ :maxlength="inputOptions.maxlength"
73
+ :name="inputOptions.name"
74
+ :readonly="inputOptions.readonly"
75
+ :tabindex="inputOptions.tabindex"
76
+ :aria-describedby="inputOptions['aria-describedby']"
77
+ @blur="onBlur"
78
+ @focus="onFocus"
79
+ @keyup.enter="onEnter"
80
+ @keyup.space="onSpace"
81
+ >
82
+ </div>
83
+ </label>
84
+ </div>
85
+ </template>
86
+
87
+ <script lang="ts" setup>
88
+ import {
89
+ Icon, Dropdown, TextInput, Flag,
90
+ allCountries, type Country,
91
+ } from '@bagelink/vue';
92
+ import { onMounted, watch } from 'vue';
93
+ import { parsePhoneNumberFromString, type CountryCode, type PhoneNumber } from 'libphonenumber-js';
94
+ import axios from 'axios';
95
+
96
+ async function getIp() {
97
+ const apiData = sessionStorage?.getItem('ipapi');
98
+ if (apiData) return JSON.parse(apiData);
99
+ const { data } = await axios.get('https://ipapi.co/json/');
100
+ sessionStorage.setItem('ipapi', JSON.stringify(data));
101
+ return data;
102
+ }
103
+
104
+ type DropdownOptions = {
105
+ disabled?: boolean;
106
+ showFlags?: boolean;
107
+ showDialCodeInSelection?: boolean;
108
+ showDialCodeInList?: boolean;
109
+ };
110
+
111
+ type InputOptions = {
112
+ autofocus?: boolean;
113
+ 'aria-describedby'?: string;
114
+ maxlength?: number;
115
+ name?: string;
116
+ placeholder?: string;
117
+ readonly?: boolean;
118
+ tabindex?: number;
119
+ showDialCode?: boolean;
120
+ };
121
+
122
+ type Props = {
123
+ label?: string;
124
+ id?: string;
125
+ autocomplete?: 'on' | 'off';
126
+ placeholder?: string;
127
+ required?: boolean;
128
+ modelValue: string;
129
+ allCountries?: Country[];
130
+ autoFormat?: boolean;
131
+ customValidate?: boolean | RegExp;
132
+ defaultCountry?: string | number;
133
+ disabled?: boolean;
134
+ searchable?: boolean;
135
+ autoDefaultCountry?: boolean;
136
+ dropdownOptions?: DropdownOptions;
137
+ excludeCountries?: string[];
138
+ inputOptions?: InputOptions;
139
+ invalidMsg?: string;
140
+ mode?: 'auto' | 'international' | 'national';
141
+ onlyCountries?: string[];
142
+ preferredCountries?: string[];
143
+ };
144
+
145
+ const props = withDefaults(defineProps<Props>(), {
146
+ modelValue: '',
147
+ autocomplete: 'on',
148
+ allCountries: () => allCountries,
149
+ autoFormat: true,
150
+ customValidate: false,
151
+ defaultCountry: '',
152
+ placeholder: 'Enter a phone number',
153
+ searchable: true,
154
+ dropdownOptions: () => ({
155
+ disabled: false,
156
+ showFlags: true,
157
+ showDialCodeInSelection: true,
158
+ showDialCodeInList: true,
159
+ searchable: true,
160
+ }),
161
+ disabled: false,
162
+ autoDefaultCountry: true,
163
+ excludeCountries: () => [],
164
+ inputOptions: () => ({
165
+ showDialCode: true,
166
+ autofocus: false,
167
+ 'aria-describedby': '',
168
+ id: '',
169
+ maxlength: 25,
170
+ name: '',
171
+ readonly: false,
172
+ tabindex: 0,
173
+ }),
174
+ required: false,
175
+ invalidMsg: 'Invalid phone number',
176
+ mode: 'auto',
177
+ onlyCountries: () => [],
178
+ preferredCountries: () => [],
179
+ });
180
+
181
+ let activeCountryCode = $ref<CountryCode>();
182
+ let open = $ref(false);
183
+ let selectedIndex = $ref<number>();
184
+ const dropdownOpenDirection = $ref('below');
185
+ const searchQuery = $ref('');
186
+
187
+ const refInput = $ref<HTMLInputElement | null>(null);
188
+
189
+ const emit = defineEmits([
190
+ 'update:modelValue',
191
+ 'change',
192
+ 'input',
193
+ 'blur',
194
+ 'focus',
195
+ 'country-changed',
196
+ 'enter',
197
+ 'space',
198
+ ]);
199
+
200
+ function formatPhone(val: string) {
201
+ let result: PhoneNumber | undefined;
202
+ if (val?.[0] === '+') result = parsePhoneNumberFromString(val);
203
+ else result = parsePhoneNumberFromString(val, activeCountryCode);
204
+ const valid = result?.isValid?.();
205
+ refInput?.setCustomValidity?.(valid ? '' : props.invalidMsg);
206
+ const formatted = valid ? result?.format?.(parsedMode.toUpperCase() as any) : val;
207
+ return formatted;
208
+ }
209
+
210
+ const cleanStr = (str?: string) => {
211
+ const phoneNumber = (str?.match(/[()\-+0-9\s]*/g) || []).join('');
212
+ return phoneNumber.trim();
213
+ };
214
+
215
+ let phone = $computed<string>({
216
+ get: () => props.modelValue,
217
+ set: (value) => {
218
+ const clean = cleanStr(value);
219
+ const formatted = formatPhone(clean) || '';
220
+ emit('update:modelValue', formatted);
221
+ },
222
+ });
223
+
224
+ // Computed: activeCountry
225
+ const activeCountry = $computed(() => props.allCountries.find((country) => country.iso2 === activeCountryCode?.toUpperCase()));
226
+
227
+ const isPreferred = (country?: Country) => props.preferredCountries.includes(country?.iso2 as string);
228
+
229
+ // Computed: parsedMode
230
+ const parsedMode = $computed(() => {
231
+ if (props.mode === 'auto') {
232
+ if (!phone || phone[0] !== '+') return 'national';
233
+ return 'international';
234
+ }
235
+ return props.mode || 'international';
236
+ });
237
+
238
+ const filteredCountries = $computed(() => {
239
+ if (props.onlyCountries.length) {
240
+ return props.allCountries.filter(({ iso2 }) => props.onlyCountries.some((c) => c.toUpperCase() === iso2));
241
+ }
242
+ if (props.excludeCountries.length) {
243
+ return props.allCountries.filter(
244
+ ({ iso2 }) => !props.excludeCountries.includes(iso2.toUpperCase()) &&
245
+ !props.excludeCountries.includes(iso2.toLowerCase()),
246
+ );
247
+ }
248
+ return props.allCountries;
249
+ });
250
+
251
+ const sortedCountries = $computed(() => {
252
+ const preferredCountries = getCountries(props.preferredCountries);
253
+
254
+ const countriesList = [...preferredCountries, ...filteredCountries];
255
+ const cleanInput = searchQuery.replace(/[~`!@#$%^&*()+={}\[\];:'"<>.,\/\\\?-_]/g, '');
256
+ return countriesList
257
+ .filter(
258
+ (c) => new RegExp(cleanInput, 'i').test(c?.name || '') ||
259
+ new RegExp(cleanInput, 'i').test(c?.iso2 || '') ||
260
+ new RegExp(cleanInput, 'i').test(c?.dialCode || ''),
261
+ )
262
+ .filter(Boolean);
263
+ });
264
+
265
+ // Watchers
266
+ watch(
267
+ () => activeCountry,
268
+ (value, oldValue) => {
269
+ if (!value && oldValue?.iso2) {
270
+ activeCountryCode = oldValue.iso2;
271
+ return;
272
+ }
273
+ if (value?.iso2) emit('country-changed', value);
274
+ },
275
+ );
276
+
277
+ const initializeCountry = async () => {
278
+ // if (phone?.[0] === '+') return;
279
+ // 2. Use default country if passed from parent
280
+ if (props.defaultCountry) {
281
+ if (typeof props.defaultCountry === 'string') {
282
+ chooseCountry(props.defaultCountry);
283
+ return;
284
+ }
285
+ if (typeof props.defaultCountry === 'number') {
286
+ const country = findCountryByDialCode(props.defaultCountry);
287
+ if (country) {
288
+ chooseCountry(country.iso2);
289
+ return;
290
+ }
291
+ }
292
+ }
293
+
294
+ const fallbackCountry = sortedCountries[0];
295
+
296
+ if (props.autoDefaultCountry) {
297
+ try {
298
+ const res = (await getIp()).country;
299
+ chooseCountry(res || activeCountryCode);
300
+ } catch (error) {
301
+ console.warn(error);
302
+ chooseCountry(fallbackCountry?.iso2);
303
+ }
304
+ } else {
305
+ // 4. Use the first country from preferred list (if available) or all countries list
306
+ chooseCountry(fallbackCountry?.iso2);
307
+ }
308
+ };
309
+
310
+ onMounted(initializeCountry);
311
+
312
+ const findCountry = (iso: string) => filteredCountries.find((country) => country.iso2 === iso?.toUpperCase());
313
+
314
+ const getCountries = (list: string[]): Country[] => {
315
+ const countryList: Country[] = [];
316
+ list.forEach((countryCode) => {
317
+ const country = findCountry(countryCode);
318
+ if (country) countryList.push(country);
319
+ });
320
+ return countryList;
321
+ };
322
+
323
+ const findCountryByDialCode = (dialCode: number) => filteredCountries.find((country) => Number(country.dialCode) === dialCode);
324
+
325
+ const chooseCountry = (country?: string) => {
326
+ if (!country) return;
327
+ const parsedCountry = findCountry(country);
328
+ if (!parsedCountry) return;
329
+ activeCountryCode = parsedCountry.iso2;
330
+
331
+ if (props.inputOptions?.showDialCode && parsedCountry) {
332
+ phone = `+${parsedCountry.dialCode}`;
333
+ activeCountryCode = parsedCountry.iso2 || '';
334
+ return;
335
+ }
336
+
337
+ activeCountryCode = parsedCountry.iso2 || '';
338
+ // emitInput(phone);
339
+ };
340
+
341
+ const onBlur = () => emit('blur');
342
+
343
+ const onFocus = () => emit('focus');
344
+
345
+ const onEnter = () => emit('enter');
346
+
347
+ const onSpace = () => emit('space');
348
+
349
+ // const focus = () => refInput?.focus();
350
+
351
+ // Method: reset
352
+ const reset = () => {
353
+ selectedIndex = sortedCountries.findIndex((c) => c.iso2 === activeCountryCode);
354
+ open = false;
355
+ };
356
+ </script>
357
+
358
+ <style scoped>
359
+ .tel-input {
360
+ direction: ltr;
361
+ text-align: left;
362
+ background: var(--input-bg);
363
+ border: none;
364
+ padding-inline-start: 0.7rem;
365
+ border-radius: var(--input-border-radius);
366
+ color: var(--input-color);
367
+ min-width: calc(var(--input-height) * 3);
368
+ width: 100%;
369
+ }
370
+
371
+ .tel-input:focus-within {
372
+ outline: none;
373
+ box-shadow: inset 0 0 10px #00000012;
374
+ }
375
+ .tel-input input {
376
+ background: transparent;
377
+ }
378
+ .tel-input input::placeholder {
379
+ color: transparent;
380
+ }
381
+ .tel-input input:focus-visible {
382
+ box-shadow: none;
383
+ }
384
+ .input_country-code {
385
+ font-size: var(--input-font-size);
386
+ color: var(--input-color);
387
+ }
388
+ .tel-country {
389
+ font-size: var(--input-font-size);
390
+ max-width: 200px;
391
+ white-space: nowrap;
392
+ text-overflow: ellipsis;
393
+ overflow: hidden;
394
+ margin-top: 0;
395
+ margin-bottom: 0;
396
+ }
397
+ .tel-countryp-dropdown {
398
+ direction: ltr;
399
+ text-align: left;
400
+ }
401
+ </style>
@@ -2,7 +2,7 @@
2
2
  <div
3
3
  class="bagel-input text-input"
4
4
  :class="{
5
- dense, small, shrink, toggleEdit, editMode, code,
5
+ dense, small, shrink, code,
6
6
  textInputIconWrap: icon,
7
7
  txtInputIconStart: iconStart,
8
8
  }"
@@ -20,11 +20,11 @@
20
20
  :rows="1"
21
21
  ref="input"
22
22
  :placeholder="placeholder || label"
23
- :disabled="!editMode"
23
+ :disabled
24
24
  :required="required"
25
25
  :pattern="pattern"
26
26
  v-bind="nativeInputAttrs"
27
- @dblclick="toggleEditAction"
27
+ @input="updateInputVal"
28
28
  >
29
29
  <textarea
30
30
  v-else
@@ -35,11 +35,11 @@
35
35
  :rows="rows"
36
36
  ref="input"
37
37
  :placeholder="placeholder || label"
38
- :disabled="!editMode"
38
+ :disabled
39
39
  :required="required"
40
40
  :pattern="pattern"
41
41
  v-bind="nativeInputAttrs"
42
- @dblclick="toggleEditAction"
42
+ @input="updateInputVal"
43
43
  />
44
44
  <p v-if="helptext">{{ helptext }}</p>
45
45
  </label>
@@ -53,14 +53,6 @@
53
53
  v-if="icon"
54
54
  :icon="icon"
55
55
  />
56
- <Btn
57
- class="toggleEditBtn"
58
- v-if="toggleEdit"
59
- thin
60
- @click="toggleEditAction"
61
- :icon="editMode ? 'check' : 'edit'"
62
- flat
63
- />
64
56
  </div>
65
57
  </template>
66
58
 
@@ -87,7 +79,7 @@ const props = withDefaults(
87
79
  required?: boolean;
88
80
  pattern?: string;
89
81
  shrink?: boolean;
90
- toggleEdit?: boolean;
82
+ disabled?: boolean;
91
83
  type?: string;
92
84
  nativeInputAttrs?: Record<string, any>;
93
85
  icon?: MaterialIcons;
@@ -102,27 +94,13 @@ const props = withDefaults(
102
94
  }>(),
103
95
  {
104
96
  type: 'text',
105
- toggleEdit: false,
106
97
  modelValue: '',
107
98
  },
108
99
  );
109
100
  let inputVal = $ref<string | number>();
110
- let editMode = $ref<boolean>(!props.toggleEdit);
111
101
 
112
102
  const input = $ref<HTMLInputElement>();
113
103
 
114
- const toggleEditAction = () => {
115
- if (!props.toggleEdit) return;
116
- if (editMode) {
117
- editMode = false;
118
- emit('debounce', inputVal);
119
- emit('update:modelValue', inputVal as string);
120
- } else {
121
- editMode = true;
122
- setTimeout(() => input?.focus(), 10);
123
- }
124
- };
125
-
126
104
  const rows = $computed(() => {
127
105
  if (props.lines) return props.lines;
128
106
  if (props.autoheight) return `${inputVal}`?.split('\n').length || 1;
@@ -130,15 +108,11 @@ const rows = $computed(() => {
130
108
  return 1;
131
109
  });
132
110
 
133
- watch(
134
- () => inputVal,
135
- (newVal) => {
136
- if (props.toggleEdit) return;
137
- if (newVal === props.modelValue) return;
138
- emit('update:modelValue', newVal as string);
139
- debounce(() => emit('debounce', newVal));
140
- },
141
- );
111
+ function updateInputVal() {
112
+ if (props.disabled) return;
113
+ emit('update:modelValue', inputVal as string);
114
+ debounce(() => emit('debounce', inputVal), 300);
115
+ }
142
116
 
143
117
  watch(
144
118
  () => props.modelValue,
@@ -185,10 +159,6 @@ onMounted(() => {
185
159
  opacity: 0.3;
186
160
  }
187
161
 
188
- .bagel-input.toggleEdit:hover {
189
- background-color: var(--input-bg);
190
- }
191
-
192
162
  .bagel-input.small {
193
163
  margin-bottom: 0;
194
164
  height: 30px;
@@ -200,15 +170,13 @@ onMounted(() => {
200
170
  gap: 0.5rem;
201
171
  }
202
172
 
203
- .bagel-input label {
204
- font-size: var(--label-font-size);
173
+ .bagel-input input:disabled {
174
+ background: #f5f5f5;
175
+
205
176
  }
206
177
 
207
- .toggleEditBtn {
208
- position: absolute;
209
- right: -24px;
210
- top: 4;
211
- opacity: 0;
178
+ .bagel-input label {
179
+ font-size: var(--label-font-size);
212
180
  }
213
181
 
214
182
  .textInputIconWrap {
@@ -235,11 +203,6 @@ onMounted(() => {
235
203
  padding-inline-start: 2rem;
236
204
  }
237
205
 
238
- .bagel-input:hover .toggleEditBtn,
239
- .bagel-input.editMode .toggleEditBtn {
240
- opacity: 1;
241
- }
242
-
243
206
  .bagel-input.small textarea {
244
207
  height: 30px;
245
208
  }
@@ -11,4 +11,5 @@ export { default as RadioPillsInput } from './RadioPillsInput.vue';
11
11
  export { default as FileUpload } from './FileUpload.vue';
12
12
  export { default as ToggleInput } from './ToggleInput.vue';
13
13
  export { default as RichText } from './RichText.vue';
14
+ export { default as TelInput } from './TelInput.vue';
14
15
  export { Dropdown } from 'floating-vue';
@@ -21,6 +21,7 @@ export { default as Badge } from './Badge.vue';
21
21
  export { default as BglVideo } from './BglVideo.vue';
22
22
  export { default as Carousel } from './Carousel.vue';
23
23
  export { default as ModalConfirm } from './ModalConfirm.vue';
24
+ export { default as Flag } from './Flag.vue';
24
25
 
25
26
  export * from './form';
26
27
  export * from './dashboard';
@@ -39,6 +39,7 @@ const tabComponent = defineComponent({
39
39
  const currentTabIndex = props.tabs.findIndex(
40
40
  (tab) => tabValue(tab) === currentTab.value,
41
41
  );
42
+ if (currentTabIndex === -1) return null;
42
43
  const slotChildren = slots?.default?.()?.[1]?.children;
43
44
 
44
45
  return h('div', (slotChildren as VNode[])[currentTabIndex]);
package/src/index.ts CHANGED
@@ -5,4 +5,5 @@ export { ModalPlugin, useModal } from './plugins/modal';
5
5
  export * from './utils';
6
6
  export * from './types';
7
7
  export * from './components';
8
+ export * from './utils/allCountries';
8
9
  import './styles/bagel.css';