@nexdom/uimed-vue 2.0.0-beta.41 → 2.0.0-beta.43

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.
@@ -652,7 +652,9 @@ type MaskFieldVariant = TextFieldVariant;
652
652
  * Masks the library provides:
653
653
  *
654
654
  * - `cpf`: `###.###.###-##` (e.g. `123.456.789-09`);
655
- * - `cnpj`: `##.###.###/####-##` (e.g. `12.345.678/0001-95`);
655
+ * - `cnpj`: `##.###.###/####-##`, where the first 12 positions also accept
656
+ * letters, typed in uppercase or lowercase and kept in uppercase (e.g.
657
+ * `12.345.678/0001-95` or the alphanumeric `12.ABC.345/01DE-35`);
656
658
  * - `cep`: `#####-###` (e.g. `01310-100`);
657
659
  * - `phone`: `(##) #####-####` (e.g. `(11) 91234-5678`);
658
660
  * - `date`: `##/##/####` (e.g. `31/12/2026`), formatting only.
@@ -691,14 +693,21 @@ type MaskFieldProps = {
691
693
  * inserted automatically (e.g. `####-##/####` or `Nº ####`). A custom mask
692
694
  * must contain at least one `#`.
693
695
  *
694
- * Only digits are accepted, up to the number of `#` in the mask, and the
695
- * `v-model` holds only the digits (e.g. `12345678909` for `cpf`). A literal
696
- * the user could also type, such as a fixed digit (`0800 ###-####`), isn't
697
- * supported: where the literal is expected, the same character is read as
698
- * the literal.
696
+ * Only digits are accepted, up to the number of `#` in the mask, except in
697
+ * the first 12 positions of `cnpj`, which also accept letters (A–Z, without
698
+ * accents, typed in uppercase or lowercase) for the alphanumeric CNPJ (only
699
+ * with `cnpj`: the custom mask `##.###.###/####-##` accepts only digits). The
700
+ * `v-model` holds only these characters, with letters in uppercase (e.g.
701
+ * `12345678909` for `cpf`, and `12345678000195` or `12ABC34501DE35` for
702
+ * `cnpj`). A literal the user could also type, such as a fixed digit
703
+ * (`0800 ###-####`), isn't supported: where the literal is expected, the
704
+ * same character is read as the literal.
699
705
  *
700
- * The `cpf` and `cnpj` masks don't validate check digits, and `cnpj` only
701
- * accepts digits (not the alphanumeric CNPJ).
706
+ * Editing in the middle of the value shifts the characters after it, and one
707
+ * that doesn't fit its new position (e.g. a letter of `cnpj` shifted into
708
+ * the last 2 positions) is discarded.
709
+ *
710
+ * The `cpf` and `cnpj` masks don't validate check digits.
702
711
  *
703
712
  * Changing it keeps the `v-model`: a value that doesn't fit the new mask
704
713
  * (e.g. longer than it) is displayed cut and fails validation with "Valor
@@ -738,7 +747,8 @@ type MaskFieldProps = {
738
747
  label?: string;
739
748
  /**
740
749
  * Field placeholder. Defaults to the mask, with `0` in place of each `#`,
741
- * e.g. `000.000.000-00` for `cpf` and `Nº 0000` for `Nº ####`.
750
+ * e.g. `000.000.000-00` for `cpf`, `00.000.000/0000-00` for `cnpj` and
751
+ * `Nº 0000` for `Nº ####`.
742
752
  */
743
753
  placeholder?: string;
744
754
  /**
@@ -755,9 +765,10 @@ type MaskFieldProps = {
755
765
  //#endregion
756
766
  //#region src/components/inputs/mask-field/mask-field.vue.d.ts
757
767
  /**
758
- * Text field that masks the value while the user types, such as CPF, CNPJ,
759
- * CEP, phone or a custom mask. Its value holds only the digits, without the
760
- * mask's literals.
768
+ * Text field that masks the value while the user types, such as CPF, CNPJ
769
+ * (numeric or alphanumeric), CEP, phone or a custom mask. Its value holds only
770
+ * the typed characters, without the mask's literals and with letters in
771
+ * uppercase.
761
772
  *
762
773
  * @example
763
774
  * ```vue
@@ -488,25 +488,41 @@ function useTextFieldType(type, isPasswordVisible) {
488
488
  });
489
489
  }
490
490
  /**
491
- * Controls the button that shows or hides a password field's value. The
492
- * password starts hidden and hides again whenever the type stops being
493
- * `password`.
491
+ * Controls the icon at the end of the field: a decorative magnifier on
492
+ * `search`, and the toggle that shows or hides the value on `password`. The
493
+ * toggle works with click, `Enter` and `Space`, does nothing while disabled,
494
+ * and the password hides again whenever the type stops being `password`.
494
495
  */
495
- function useTextFieldPasswordToggle(type) {
496
+ function useTextFieldAppendIcon(type, disabled) {
496
497
  const isPasswordField = computed(() => toValue(type) === "password");
497
498
  const isPasswordVisible = ref(false);
498
- const icon = computed(() => isPasswordVisible.value ? "mdi-eye-off" : "mdi-eye");
499
499
  watch(isPasswordField, () => {
500
500
  isPasswordVisible.value = false;
501
501
  });
502
502
  function toggle() {
503
+ if (toValue(disabled)) return;
503
504
  isPasswordVisible.value = !isPasswordVisible.value;
504
505
  }
506
+ function onKeydown(event) {
507
+ if (event.key !== "Enter" && event.key !== " ") return;
508
+ event.preventDefault();
509
+ event.stopPropagation();
510
+ toggle();
511
+ }
512
+ const iconProps = computed(() => {
513
+ if (toValue(type) === "search") return { icon: "mdi-magnify" };
514
+ if (!isPasswordField.value) return void 0;
515
+ return {
516
+ icon: isPasswordVisible.value ? "mdi-eye-off" : "mdi-eye",
517
+ "aria-label": "Mostrar senha",
518
+ "aria-pressed": isPasswordVisible.value,
519
+ onClick: toggle,
520
+ onKeydown
521
+ };
522
+ });
505
523
  return {
506
- isPasswordField,
507
524
  isPasswordVisible: readonly(isPasswordVisible),
508
- icon,
509
- toggle
525
+ iconProps
510
526
  };
511
527
  }
512
528
  function useTextFieldRules(props) {
@@ -614,12 +630,10 @@ var text_field_default = /* @__PURE__ */ defineComponent({
614
630
  const modelValue = useModel(__props, "modelValue");
615
631
  const props = __props;
616
632
  const vuetifyVariant = useTextFieldVariant(() => props.variant);
617
- const { isPasswordField, isPasswordVisible, icon: passwordToggleIcon, toggle: togglePassword } = useTextFieldPasswordToggle(() => props.type);
633
+ const { isPasswordVisible, iconProps: appendIconProps } = useTextFieldAppendIcon(() => props.type, () => props.disabled);
618
634
  const vuetifyType = useTextFieldType(() => props.type, isPasswordVisible);
619
635
  const rules = useTextFieldRules(props);
620
- const isSearchField = computed(() => props.type === "search");
621
- const computedClearable = computed(() => props.clearable ?? isSearchField.value);
622
- const appendIcon = computed(() => isSearchField.value ? "mdi-magnify" : void 0);
636
+ const computedClearable = computed(() => props.clearable ?? props.type === "search");
623
637
  function clear() {
624
638
  modelValue.value = "";
625
639
  }
@@ -629,7 +643,6 @@ var text_field_default = /* @__PURE__ */ defineComponent({
629
643
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => modelValue.value = $event),
630
644
  variant: unref(vuetifyVariant),
631
645
  type: unref(vuetifyType),
632
- "append-inner-icon": appendIcon.value,
633
646
  rules: unref(rules),
634
647
  "data-testid": props.dataTestid,
635
648
  disabled: props.disabled,
@@ -644,28 +657,14 @@ var text_field_default = /* @__PURE__ */ defineComponent({
644
657
  }, createSlots({
645
658
  loader: withCtx((loader) => [createVNode(field_loader_default, normalizeProps(guardReactiveProps(loader)), null, 16)]),
646
659
  _: 2
647
- }, [unref(isPasswordField) ? {
660
+ }, [unref(appendIconProps) ? {
648
661
  name: "append-inner",
649
- fn: withCtx(() => [createVNode(unref(VBtn), {
650
- icon: unref(passwordToggleIcon),
651
- "aria-label": "Mostrar senha",
652
- "aria-pressed": unref(isPasswordVisible),
653
- disabled: props.disabled,
654
- size: "small",
655
- variant: "text",
656
- onClick: unref(togglePassword)
657
- }, null, 8, [
658
- "icon",
659
- "aria-pressed",
660
- "disabled",
661
- "onClick"
662
- ])]),
662
+ fn: withCtx(() => [createVNode(unref(VIcon), mergeProps(unref(appendIconProps), { disabled: props.disabled }), null, 16, ["disabled"])]),
663
663
  key: "0"
664
664
  } : void 0]), 1032, [
665
665
  "modelValue",
666
666
  "variant",
667
667
  "type",
668
- "append-inner-icon",
669
668
  "rules",
670
669
  "data-testid",
671
670
  "disabled",
@@ -1918,79 +1917,96 @@ var date_time_field_default = /* @__PURE__ */ defineComponent({
1918
1917
  //#endregion
1919
1918
  //#region src/composables/inputs/mask-field.ts
1920
1919
  /**
1921
- * Masks of the presets. Our preset names always map to these masks, never to
1922
- * Vuetify's own presets (e.g. its American `phone`).
1920
+ * Masks of the presets, as Vuetify reads them: `#` is a digit and `N` is a
1921
+ * letter or a digit, with letters converted to uppercase. Our preset names
1922
+ * always map to these masks, never to Vuetify's own presets (e.g. its American
1923
+ * `phone`).
1923
1924
  */
1924
1925
  const maskFieldPresets = {
1925
1926
  cpf: "###.###.###-##",
1926
- cnpj: "##.###.###/####-##",
1927
+ cnpj: "NN.NNN.NNN/NNNN-##",
1927
1928
  cep: "#####-###",
1928
1929
  phone: "(##) #####-####",
1929
1930
  date: "##/##/####"
1930
1931
  };
1931
1932
  /**
1932
- * Returns the mask of a preset, or the given mask itself when it's a custom
1933
- * one.
1933
+ * Returns the mask as Vuetify reads it: the mask of a preset, or the given mask
1934
+ * itself, escaped, when it's a custom one.
1934
1935
  *
1935
1936
  * It takes any string, as the `mask` prop may get one through a cast or from
1936
1937
  * JavaScript: a string that isn't a preset is taken as the mask.
1937
1938
  */
1938
1939
  function resolveMask(mask) {
1939
- return isMaskFieldPreset(mask) ? maskFieldPresets[mask] : mask;
1940
+ return isMaskFieldPreset(mask) ? maskFieldPresets[mask] : toVuetifyMask(mask);
1940
1941
  }
1941
1942
  /**
1942
- * Returns a mask as Vuetify reads it. `#` is our only token, but Vuetify always
1943
- * adds its own tokens (`A`, `a`, `N`, `n` and `X`) and its escape (`\`), so
1944
- * these characters are escaped to stay literals.
1943
+ * Returns a custom mask as Vuetify reads it. `#` is the only token of custom
1944
+ * masks, but Vuetify always adds its own tokens (`A`, `a`, `N`, `n` and `X`)
1945
+ * and its escape (`\`), so these characters are escaped to stay literals.
1945
1946
  */
1946
1947
  function toVuetifyMask(mask) {
1947
1948
  return mask.replaceAll(/[AaNnX\\]/g, String.raw`\$&`);
1948
1949
  }
1949
1950
  /**
1950
- * Returns the default placeholder of a mask: the mask with `0` in place of each
1951
- * `#`, keeping its literals (e.g. `000.000.000-00` for `cpf`).
1951
+ * Returns the tokens of a mask as Vuetify reads it (see {@link resolveMask}),
1952
+ * in order, without its literals (e.g. `NNNNNNNNNNNN##` for `cnpj`).
1953
+ */
1954
+ function getMaskTokens(mask) {
1955
+ return mask.replaceAll(/\\.|[^#N]/g, "");
1956
+ }
1957
+ /**
1958
+ * Returns the default placeholder of a mask as Vuetify reads it (see
1959
+ * {@link resolveMask}): the mask with `0` in place of each token, keeping its
1960
+ * literals (e.g. `000.000.000-00` for `cpf` and `Nº 0000` for `Nº ####`).
1952
1961
  */
1953
1962
  function getMaskPlaceholder(mask) {
1954
- return mask.replaceAll("#", "0");
1963
+ return mask.replaceAll(/\\(.)|[#N]/g, (_token, literal) => literal ?? "0");
1955
1964
  }
1956
1965
  function isMaskFieldPreset(mask) {
1957
1966
  return Object.hasOwn(maskFieldPresets, mask);
1958
1967
  }
1959
1968
  /**
1960
- * Keeps the input masked while the user edits it, emitting only the digits
1961
- * (Vuetify's `unmask`).
1969
+ * Keeps the input masked while the user edits it, emitting only the
1970
+ * characters of the tokens (Vuetify's `unmask`), with letters in uppercase.
1962
1971
  *
1963
- * A `v-model` out of the mask's format (masked, or with characters or length
1964
- * the mask doesn't accept) is displayed normalized, without being emitted, and
1965
- * validated as it is, so a value that doesn't fit the mask fails validation.
1972
+ * A `v-model` out of the mask's format (masked, with lowercase letters, or with
1973
+ * characters or length the mask doesn't accept) is displayed normalized,
1974
+ * without being emitted, and validated as it is, so a value that doesn't fit
1975
+ * the mask fails validation.
1966
1976
  */
1967
1977
  function useMaskField(props, model) {
1968
1978
  const mask = computed(() => resolveMask(props.mask));
1969
- const digitCount = computed(() => mask.value.split("#").length - 1);
1979
+ const tokens = computed(() => getMaskTokens(mask.value));
1980
+ const acceptsLetters = computed(() => tokens.value.includes("N"));
1970
1981
  const vuetifyMask = useMask({ get mask() {
1971
1982
  return {
1972
- mask: toVuetifyMask(mask.value),
1983
+ mask: mask.value,
1973
1984
  tokens: {}
1974
1985
  };
1975
1986
  } });
1976
1987
  const displayValue = computed(() => vuetifyMask.mask(unmask(model.value)));
1977
1988
  const placeholder = computed(() => props.placeholder ?? getMaskPlaceholder(mask.value));
1989
+ const inputMode = computed(() => acceptsLetters.value ? "text" : "numeric");
1990
+ const autocapitalize = computed(() => acceptsLetters.value ? "characters" : void 0);
1978
1991
  const defaultRules = useRules(props);
1979
1992
  const rules = computed(() => [
1980
1993
  ...defaultRules.value,
1981
- (value) => !value || !vuetifyMask.isValid(value) || isComplete(value) || "Valor incompleto",
1982
- (value) => !value || vuetifyMask.isValid(value) || "Valor inválido"
1994
+ (value) => !value || !isValid(value) || isComplete(value) || "Valor incompleto",
1995
+ (value) => !value || isValid(value) || "Valor inválido"
1983
1996
  ]);
1984
1997
  function unmask(text) {
1985
1998
  return vuetifyMask.unmask(text) ?? "";
1986
1999
  }
2000
+ function isValid(value) {
2001
+ return unmask(value).toUpperCase() === unmask(vuetifyMask.mask(value));
2002
+ }
1987
2003
  function isComplete(value) {
1988
- return unmask(vuetifyMask.mask(value)).length === digitCount.value;
2004
+ return unmask(vuetifyMask.mask(value)).length === tokens.value.length;
1989
2005
  }
1990
2006
  /**
1991
2007
  * Returns the position in a masked text right after the given number of
1992
- * digits, skipping the literals after it unless the user deleted backwards
1993
- * (as Vuetify's mask input does).
2008
+ * characters of the value, skipping the literals after it unless the user
2009
+ * deleted backwards (as Vuetify's mask input does).
1994
2010
  */
1995
2011
  function findCaret(masked, valueLength, deletedBackwards) {
1996
2012
  let caret = 0;
@@ -2020,6 +2036,8 @@ function useMaskField(props, model) {
2020
2036
  return {
2021
2037
  displayValue,
2022
2038
  placeholder,
2039
+ inputMode,
2040
+ autocapitalize,
2023
2041
  rules,
2024
2042
  onInput,
2025
2043
  onCompositionEnd
@@ -2088,7 +2106,7 @@ var mask_field_default = /* @__PURE__ */ defineComponent({
2088
2106
  const modelValue = useModel(__props, "modelValue");
2089
2107
  const props = __props;
2090
2108
  const vuetifyVariant = useTextFieldVariant(() => props.variant);
2091
- const { displayValue, placeholder, rules, onInput, onCompositionEnd } = useMaskField(props, modelValue);
2109
+ const { displayValue, placeholder, inputMode, autocapitalize, rules, onInput, onCompositionEnd } = useMaskField(props, modelValue);
2092
2110
  function keepFieldControlled() {}
2093
2111
  function clear() {
2094
2112
  modelValue.value = "";
@@ -2109,7 +2127,8 @@ var mask_field_default = /* @__PURE__ */ defineComponent({
2109
2127
  hint: props.hint,
2110
2128
  clearable: props.clearable,
2111
2129
  type: "text",
2112
- inputmode: "numeric",
2130
+ inputmode: unref(inputMode),
2131
+ autocapitalize: unref(autocapitalize),
2113
2132
  "onUpdate:modelValue": keepFieldControlled,
2114
2133
  onInput: unref(onInput),
2115
2134
  onCompositionend: unref(onCompositionEnd),
@@ -2131,6 +2150,8 @@ var mask_field_default = /* @__PURE__ */ defineComponent({
2131
2150
  "placeholder",
2132
2151
  "hint",
2133
2152
  "clearable",
2153
+ "inputmode",
2154
+ "autocapitalize",
2134
2155
  "onInput",
2135
2156
  "onCompositionend"
2136
2157
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexdom/uimed-vue",
3
- "version": "2.0.0-beta.41",
3
+ "version": "2.0.0-beta.43",
4
4
  "description": "A ui framework for NEXDOM applications.",
5
5
  "keywords": [
6
6
  "lib",