@antify/ui 4.1.37 → 4.1.38

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,347 @@
1
+ <script lang="ts" setup>
2
+ import {
3
+ computed, watch,
4
+ } from 'vue';
5
+ import {
6
+ useVModel,
7
+ } from '@vueuse/core';
8
+ import AntField from '../forms/AntField.vue';
9
+ import AntCountryInput from './AntCountryInput.vue';
10
+ import AntBaseInput from './Elements/AntBaseInput.vue';
11
+ import {
12
+ Size, InputState, Grouped,
13
+ } from '../../enums';
14
+ import {
15
+ BaseInputType,
16
+ } from './Elements/__types';
17
+ import {
18
+ COUNTRIES, CountryValueKey, Locale,
19
+ } from '../../constants/countries';
20
+ import type {
21
+ Country,
22
+ } from '../../types';
23
+ import {
24
+ ref, nextTick,
25
+ } from 'vue';
26
+
27
+ const phoneInputNativeRef = ref<HTMLInputElement | null>(null);
28
+
29
+ defineOptions({
30
+ inheritAttrs: false,
31
+ });
32
+
33
+ const props = withDefaults(defineProps<{
34
+ modelValue: string | null;
35
+ countryValue: string | number | null;
36
+ countries?: Country[];
37
+
38
+ //Common Props
39
+ size?: Size;
40
+ state?: InputState;
41
+ disabled?: boolean;
42
+ readonly?: boolean;
43
+ skeleton?: boolean;
44
+
45
+ //AntField Props
46
+ label?: string;
47
+ description?: string;
48
+ messages?: string[];
49
+
50
+ //AntCountryInput Props
51
+ countryPlaceholder?: string;
52
+ searchPlaceholder?: string;
53
+ searchable?: boolean;
54
+ countryMaxHeight?: string;
55
+ countryValueKey?: CountryValueKey;
56
+ countryErrorMessage?: string;
57
+ countrySortable?: boolean;
58
+
59
+ //AntBaseInput Props
60
+ placeholder?: string;
61
+ nullable?: boolean;
62
+ locale?: Locale;
63
+ }>(), {
64
+ size: Size.md,
65
+ state: InputState.base,
66
+ searchable: true,
67
+ searchPlaceholder: 'Search country...',
68
+ countryPlaceholder: 'Select country',
69
+ placeholder: 'Enter phone number',
70
+ countryValueKey: CountryValueKey.dialCode,
71
+ countryErrorMessage: 'Please select a country code or start with "+"',
72
+ countrySortable: true,
73
+ messages: () => [],
74
+ nullable: true,
75
+ countries: () => COUNTRIES,
76
+ locale: Locale.en,
77
+ });
78
+
79
+ const emit = defineEmits([
80
+ 'update:modelValue',
81
+ 'update:countryValue',
82
+ 'select-country',
83
+ 'validate',
84
+ 'blur',
85
+ ]);
86
+
87
+ const _countryValue = useVModel(props, 'countryValue', emit);
88
+ const _phoneNumber = useVModel(props, 'modelValue', emit);
89
+
90
+ const updateFullValue = (countryId: string | number | null, rawPhone: string | null) => {
91
+ if (!rawPhone) {
92
+ _phoneNumber.value = null;
93
+
94
+ return;
95
+ }
96
+
97
+ const country = props.countries.find(c => String(c[props.countryValueKey]) === String(countryId));
98
+
99
+ if (country && !rawPhone.startsWith('+')) {
100
+ const digitsOnly = rawPhone.replace(/\D/g, '');
101
+ _phoneNumber.value = `${country.dialCode}${digitsOnly}`;
102
+ } else {
103
+ _phoneNumber.value = rawPhone;
104
+ }
105
+ };
106
+
107
+ const showCountryError = computed(() => {
108
+ const val = props.modelValue || '';
109
+
110
+ return props.countryValue == null && val.length > 0 && !val.startsWith('+');
111
+ });
112
+
113
+ const allMessages = computed(() => {
114
+ const msgs = [
115
+ ...(props.messages || []),
116
+ ];
117
+
118
+ if (showCountryError.value) {
119
+ msgs.push(props.countryErrorMessage);
120
+ }
121
+
122
+ return msgs;
123
+ });
124
+
125
+ const currentCountry = computed(() => {
126
+ return props.countries.find(c => String(c[props.countryValueKey]) === String(props.countryValue));
127
+ });
128
+
129
+ const sortedCountriesByDialCode = computed(() => {
130
+ return [
131
+ ...props.countries,
132
+ ].sort((a, b) => b.dialCode.length - a.dialCode.length);
133
+ });
134
+
135
+ const findCountryByPhone = (phone: string): Country | undefined => {
136
+ if (!phone.startsWith('+')) {
137
+ return undefined;
138
+ }
139
+
140
+ return sortedCountriesByDialCode.value.find(country => phone.startsWith(country.dialCode));
141
+ };
142
+
143
+ const formattedNumber = computed({
144
+ get: () => {
145
+ const fullVal = props.modelValue || '';
146
+ const country = currentCountry.value;
147
+
148
+ if (country && fullVal.startsWith(country.dialCode)) {
149
+ const shortNumber = fullVal.slice(country.dialCode.length);
150
+
151
+ return country.mask ? formatByMask(shortNumber, country.mask) : shortNumber;
152
+ }
153
+
154
+ return fullVal;
155
+ },
156
+ set: (val: string | null) => {
157
+ if (!val) {
158
+ _phoneNumber.value = null;
159
+
160
+ return;
161
+ }
162
+
163
+ if (val.startsWith('+')) {
164
+ const country = findCountryByPhone(val);
165
+
166
+ if (country) {
167
+ _countryValue.value = country[props.countryValueKey] as string | number;
168
+ }
169
+
170
+ _phoneNumber.value = val;
171
+
172
+ return;
173
+ }
174
+
175
+ updateFullValue(_countryValue.value, val);
176
+ },
177
+ });
178
+
179
+ const formatByMask = (value: string | null, mask: string): string | null => {
180
+ if (!value) {
181
+ return null;
182
+ }
183
+
184
+ const digits = value.replace(/\D/g, '');
185
+ if (digits.length === 0) {
186
+ return null;
187
+ }
188
+
189
+ let result = '';
190
+ let digitIndex = 0;
191
+
192
+ for (let i = 0; i < mask.length; i++) {
193
+ if (mask[i] === '#') {
194
+ if (digitIndex < digits.length) {
195
+ result += digits[digitIndex];
196
+ digitIndex++;
197
+ } else {
198
+ break;
199
+ }
200
+ } else if (digitIndex < digits.length) {
201
+ result += mask[i];
202
+ }
203
+ }
204
+
205
+ if (digitIndex < digits.length) {
206
+ result += digits.substring(digitIndex);
207
+ }
208
+
209
+ return result || null;
210
+ };
211
+
212
+ function onCountrySelect(country: Country) {
213
+ emit('select-country', country);
214
+
215
+ nextTick(() => {
216
+ phoneInputNativeRef.value?.focus();
217
+ });
218
+ }
219
+
220
+ function onKeyPress(event: KeyboardEvent) {
221
+ const charStr = event.key;
222
+ const target = event.target as HTMLInputElement;
223
+ const currentRawValue = target.value;
224
+
225
+ if (event.ctrlKey || event.metaKey || charStr.length > 1) {
226
+ return;
227
+ }
228
+
229
+ if (!/[\d+]/.test(charStr)) {
230
+ event.preventDefault();
231
+
232
+ return;
233
+ }
234
+
235
+ if (props.countryValue && charStr === '+') {
236
+ event.preventDefault();
237
+
238
+ return;
239
+ }
240
+
241
+ if (!props.countryValue && charStr === '+' && currentRawValue.length > 0) {
242
+ event.preventDefault();
243
+ }
244
+
245
+ if (charStr === '+' && currentRawValue.includes('+')) {
246
+ event.preventDefault();
247
+ }
248
+ }
249
+
250
+ function onPaste(event: ClipboardEvent) {
251
+ event.preventDefault();
252
+ const pasteData = event.clipboardData?.getData('text') || '';
253
+
254
+ if (!pasteData) {
255
+ return;
256
+ }
257
+
258
+ const cleanInput = pasteData.replace(/[^\d+]/g, '');
259
+
260
+ if (cleanInput.startsWith('+')) {
261
+ const country = findCountryByPhone(cleanInput);
262
+
263
+ if (country) {
264
+ _countryValue.value = country[props.countryValueKey] as string | number;
265
+ }
266
+ _phoneNumber.value = cleanInput;
267
+ } else {
268
+ updateFullValue(_countryValue.value, cleanInput);
269
+ }
270
+ }
271
+
272
+ watch(_countryValue, (newCountryId, oldCountryId) => {
273
+ if (newCountryId === oldCountryId) {
274
+ return;
275
+ }
276
+
277
+ const fullVal = props.modelValue || '';
278
+ const oldCountry = props.countries.find(c => String(c[props.countryValueKey]) === String(oldCountryId));
279
+ let body = fullVal;
280
+
281
+ if (oldCountry && fullVal.startsWith(oldCountry.dialCode)) {
282
+ body = fullVal.slice(oldCountry.dialCode.length);
283
+ }
284
+
285
+ updateFullValue(newCountryId, body);
286
+ });
287
+ </script>
288
+
289
+ <template>
290
+ <AntField
291
+ :label="label"
292
+ :messages="allMessages"
293
+ :state="showCountryError ? InputState.danger : state"
294
+ :size="size"
295
+ :skeleton="skeleton"
296
+ :description="description"
297
+ data-e2e="phone-input"
298
+ >
299
+ <div
300
+ class="flex relative w-full"
301
+ @click.prevent
302
+ >
303
+ <AntCountryInput
304
+ v-model="_countryValue"
305
+ :countries="countries"
306
+ :size="size"
307
+ :locale="locale"
308
+ :state="showCountryError ? InputState.danger : state"
309
+ :disabled="disabled"
310
+ :readonly="readonly"
311
+ :skeleton="skeleton"
312
+ :searchable="searchable"
313
+ :placeholder="countryPlaceholder"
314
+ :search-placeholder="searchPlaceholder"
315
+ :max-height="countryMaxHeight"
316
+ :is-grouped="true"
317
+ :grouped="Grouped.left"
318
+ class="w-fit flex-shrink-0"
319
+ :show-dial-code-in-menu="true"
320
+ :option-value-key="countryValueKey"
321
+ :sortable="countrySortable"
322
+ @select="onCountrySelect"
323
+ />
324
+
325
+ <AntBaseInput
326
+ v-model="formattedNumber"
327
+ v-model:input-ref="phoneInputNativeRef"
328
+ :nullable="nullable"
329
+ :type="BaseInputType.text"
330
+ :state="showCountryError ? InputState.danger : state"
331
+ :size="size"
332
+ :skeleton="skeleton"
333
+ v-bind="$attrs"
334
+ :disabled="disabled"
335
+ :readonly="readonly"
336
+ :placeholder="placeholder"
337
+ :grouped="Grouped.right"
338
+ wrapper-class="flex-grow"
339
+ class="-ml-px"
340
+ @validate="val => $emit('validate', val)"
341
+ @blur="e => $emit('blur', e)"
342
+ @keydown="onKeyPress"
343
+ @paste="onPaste"
344
+ />
345
+ </div>
346
+ </AntField>
347
+ </template>
@@ -30,6 +30,7 @@ const emit = defineEmits([
30
30
  'update:modelValue',
31
31
  'update:focused',
32
32
  'selectElement',
33
+ 'clickOutside',
33
34
  ]);
34
35
  const props = withDefaults(defineProps<{
35
36
  modelValue: string | string[] | number | number[] | null;
@@ -76,6 +77,7 @@ onClickOutside(floating, () => {
76
77
  }
77
78
 
78
79
  emit('update:open', false);
80
+ emit('clickOutside');
79
81
  });
80
82
 
81
83
  const _modelValue = useVModel(props, 'modelValue', emit);
@@ -0,0 +1,16 @@
1
+ import { type Meta, type StoryObj } from '@storybook/vue3';
2
+ import AntCountryInput from '../AntCountryInput.vue';
3
+ declare const meta: Meta<typeof AntCountryInput>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof AntCountryInput>;
6
+ export declare const Docs: Story;
7
+ export declare const ValueKeyNumericCode: Story;
8
+ export declare const DefaultCountry: Story;
9
+ export declare const DefaultByNumericCode: Story;
10
+ export declare const Localization: Story;
11
+ export declare const WithoutSearch: Story;
12
+ export declare const WithoutFlags: Story;
13
+ export declare const GroupedMode: Story;
14
+ export declare const GermanEmptyState: Story;
15
+ export declare const Skeleton: Story;
16
+ export declare const summary: Story;
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.summary = exports.default = exports.WithoutSearch = exports.WithoutFlags = exports.ValueKeyNumericCode = exports.Skeleton = exports.Localization = exports.GroupedMode = exports.GermanEmptyState = exports.Docs = exports.DefaultCountry = exports.DefaultByNumericCode = void 0;
7
+ var _AntCountryInput = _interopRequireDefault(require("../AntCountryInput.vue"));
8
+ var _AntField = _interopRequireDefault(require("../../../components/forms/AntField.vue"));
9
+ var _vue = require("vue");
10
+ var _enums = require("../../../enums");
11
+ var _countries = require("../../../constants/countries");
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
+ const meta = {
14
+ title: "Inputs/Country Input",
15
+ component: _AntCountryInput.default,
16
+ parameters: {
17
+ controls: {
18
+ sort: "requiredFirst"
19
+ }
20
+ },
21
+ argTypes: {
22
+ state: {
23
+ control: "select",
24
+ options: Object.values(_enums.InputState)
25
+ },
26
+ size: {
27
+ control: "select",
28
+ options: Object.values(_enums.Size)
29
+ },
30
+ grouped: {
31
+ control: {
32
+ type: "select"
33
+ },
34
+ options: Object.values(_enums.Grouped),
35
+ description: "Where is this fields position in a group"
36
+ },
37
+ locale: {
38
+ control: {
39
+ type: "select"
40
+ },
41
+ options: Object.values(_countries.Locale),
42
+ description: "Language for country labels"
43
+ },
44
+ optionValueKey: {
45
+ control: {
46
+ type: "select"
47
+ },
48
+ options: Object.values(_countries.CountryValueKey)
49
+ },
50
+ countries: {
51
+ table: {
52
+ disable: true
53
+ }
54
+ }
55
+ }
56
+ };
57
+ module.exports = meta;
58
+ const MainRender = args => ({
59
+ components: {
60
+ AntCountryInput: _AntCountryInput.default
61
+ },
62
+ setup() {
63
+ const modelValue = (0, _vue.ref)(args.modelValue);
64
+ return {
65
+ args,
66
+ modelValue
67
+ };
68
+ },
69
+ template: `
70
+ <div>
71
+ <AntCountryInput v-bind="args" v-model="modelValue" />
72
+
73
+ <div class="mt-2 text-md text-base-400">
74
+ Selected Value ({{ args.optionValueKey || 'default' }}):
75
+ <span class="text-blue-500 font-bold">{{ modelValue === null ? 'null' : modelValue }}</span>
76
+ </div>
77
+ </div>
78
+ `
79
+ });
80
+ const Docs = exports.Docs = {
81
+ render: MainRender,
82
+ args: {
83
+ modelValue: null,
84
+ searchable: true,
85
+ countries: _countries.COUNTRIES
86
+ }
87
+ };
88
+ const ValueKeyNumericCode = exports.ValueKeyNumericCode = {
89
+ render: MainRender,
90
+ args: {
91
+ ...Docs.args,
92
+ label: "Value as Numeric Code",
93
+ description: "Using the numericCode field from the data as the model value.",
94
+ optionValueKey: _countries.CountryValueKey.numericCode,
95
+ countries: _countries.COUNTRIES
96
+ }
97
+ };
98
+ const DefaultCountry = exports.DefaultCountry = {
99
+ render: MainRender,
100
+ args: {
101
+ ...Docs.args,
102
+ label: "Default Country Logic",
103
+ description: 'Preselects Germany (DE) by providing modelValue: "DE" to the component.',
104
+ modelValue: "DE"
105
+ }
106
+ };
107
+ const DefaultByNumericCode = exports.DefaultByNumericCode = {
108
+ render: MainRender,
109
+ args: {
110
+ ...Docs.args,
111
+ label: "Default by Numeric Code",
112
+ description: "Using numericCode: 33 (France) as the default value.",
113
+ optionValueKey: _countries.CountryValueKey.numericCode,
114
+ modelValue: 33
115
+ }
116
+ };
117
+ const Localization = exports.Localization = {
118
+ render: args => ({
119
+ components: {
120
+ AntCountryInput: _AntCountryInput.default
121
+ },
122
+ setup() {
123
+ const modelValue = (0, _vue.ref)("DE");
124
+ return {
125
+ args,
126
+ modelValue
127
+ };
128
+ },
129
+ template: `
130
+ <div class="flex flex-col gap-4">
131
+ <div>
132
+ <p class="text-xs text-base-400 mb-1">Current Locale: <b>{{ args.locale }}</b></p>
133
+ <AntCountryInput v-bind="args" v-model="modelValue" />
134
+ </div>
135
+
136
+ <div class="p-4 border border-dashed border-base-300 rounded-md bg-base-50">
137
+ <p class="text-sm italic text-base-500">
138
+ Try switching the <b>locale</b> control in the panel below to see labels change between
139
+ "Germany" (EN) and "Deutschland" (DE).
140
+ </p>
141
+ </div>
142
+ </div>
143
+ `
144
+ }),
145
+ args: {
146
+ ...Docs.args,
147
+ label: "Localized Selector",
148
+ locale: _countries.Locale.de,
149
+ searchPlaceholder: "Land suchen...",
150
+ description: "The labels and search logic adapt based on the provided locale."
151
+ }
152
+ };
153
+ const WithoutSearch = exports.WithoutSearch = {
154
+ render: MainRender,
155
+ args: {
156
+ ...Docs.args,
157
+ label: "No Search Field",
158
+ searchable: false
159
+ }
160
+ };
161
+ const WithoutFlags = exports.WithoutFlags = {
162
+ render: MainRender,
163
+ args: {
164
+ ...Docs.args,
165
+ label: "No Flags Mode",
166
+ showFlags: false
167
+ }
168
+ };
169
+ const GroupedMode = exports.GroupedMode = {
170
+ render: args => ({
171
+ components: {
172
+ AntCountryInput: _AntCountryInput.default,
173
+ AntField: _AntField.default
174
+ },
175
+ setup() {
176
+ const modelValue = (0, _vue.ref)(args.modelValue);
177
+ return {
178
+ args,
179
+ modelValue
180
+ };
181
+ },
182
+ template: `
183
+ <div>
184
+ <AntField
185
+ :label="args.label"
186
+ :description="args.description"
187
+ :size="args.size"
188
+ :state="args.state"
189
+ >
190
+ <div class="flex items-center">
191
+ <AntCountryInput
192
+ v-bind="args"
193
+ v-model="modelValue"
194
+ class="w-fit flex-shrink-0"
195
+ />
196
+
197
+ <div
198
+ class="flex-grow border border-l-0 p-2 text-sm text-base-400 bg-base-50 border-base-300 rounded-r-md h-[36px] flex items-center"
199
+ >
200
+ Input area...
201
+ </div>
202
+ </div>
203
+ </AntField>
204
+
205
+ <div class="mt-2 text-md text-base-400">
206
+ Value: {{ modelValue || null }}
207
+ </div>
208
+ </div>
209
+ `
210
+ }),
211
+ args: {
212
+ ...Docs.args,
213
+ label: "Grouped Mode (Phone Input Style)",
214
+ description: "When isGrouped is true, label and description must be provided by a parent AntField.",
215
+ isGrouped: true,
216
+ grouped: _enums.Grouped.left,
217
+ modelValue: "DE",
218
+ countries: _countries.COUNTRIES
219
+ }
220
+ };
221
+ const GermanEmptyState = exports.GermanEmptyState = {
222
+ render: MainRender,
223
+ args: {
224
+ ...Docs.args,
225
+ label: "German Empty State",
226
+ description: 'Custom empty state message: "Keine L\xE4nder gefunden". Open the dropdown to see it.',
227
+ searchPlaceholder: "Land suchen...",
228
+ emptyStateMessage: "Keine L\xE4nder gefunden",
229
+ countries: []
230
+ }
231
+ };
232
+ const Skeleton = exports.Skeleton = {
233
+ render: MainRender,
234
+ args: {
235
+ ...Docs.args,
236
+ skeleton: true
237
+ }
238
+ };
239
+ const summary = exports.summary = {
240
+ parameters: {
241
+ chromatic: {
242
+ disableSnapshot: false
243
+ }
244
+ },
245
+ render: args => ({
246
+ components: {
247
+ AntCountryInput: _AntCountryInput.default
248
+ },
249
+ setup() {
250
+ const val = (0, _vue.ref)("DE");
251
+ return {
252
+ args,
253
+ val,
254
+ InputState: _enums.InputState,
255
+ Size: _enums.Size,
256
+ COUNTRIES: _countries.COUNTRIES
257
+ };
258
+ },
259
+ template: `
260
+ <div class="p-4 flex flex-col gap-6">
261
+ <div class="flex flex-wrap gap-4">
262
+ <AntCountryInput v-bind="args" v-model="val" :countries="COUNTRIES" :state="InputState.base" label="Base" class="w-64"/>
263
+ <AntCountryInput v-bind="args" v-model="val" :countries="COUNTRIES" :state="InputState.info" label="Info" class="w-64"/>
264
+ <AntCountryInput v-bind="args" v-model="val" :countries="COUNTRIES" :state="InputState.success" label="Success" class="w-64"/>
265
+ <AntCountryInput v-bind="args" v-model="val" :countries="COUNTRIES" :state="InputState.warning" label="Warning" class="w-64"/>
266
+ <AntCountryInput v-bind="args" v-model="val" :countries="COUNTRIES" :state="InputState.danger" label="Danger" class="w-64"/>
267
+ </div>
268
+
269
+ <div class="flex items-end gap-4">
270
+ <AntCountryInput v-bind="args" v-model="val" :countries="COUNTRIES" :size="Size.sm" label="Small" class="w-64"/>
271
+ <AntCountryInput v-bind="args" v-model="val" :countries="COUNTRIES" :size="Size.md" label="Medium" class="w-64"/>
272
+ <AntCountryInput v-bind="args" v-model="val" :countries="COUNTRIES" :size="Size.lg" label="Large" class="w-64"/>
273
+ </div>
274
+
275
+ <div class="flex gap-4">
276
+ <AntCountryInput v-bind="args" :countries="COUNTRIES" model-value="FR" disabled label="Disabled" class="w-64" />
277
+ <AntCountryInput v-bind="args" :countries="COUNTRIES" model-value="FR" readonly label="Readonly" class="w-64" />
278
+ <AntCountryInput v-bind="args" :countries="COUNTRIES" model-value="FR" skeleton label="Skeleton" class="w-64" />
279
+ </div>
280
+ </div>
281
+ `
282
+ }),
283
+ args: {
284
+ searchPlaceholder: "Search..."
285
+ }
286
+ };