@kigi/components 1.62.7-beta.1 → 1.62.9-beta.1
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.
- package/package.json +1 -1
- package/src/components/mbg-input-cnpj/mbg-input-cnpj.ts +58 -64
- package/src/components/mbg-input-cpfcnpj/index.ts +2 -2
- package/src/components/mbg-input-cpfcnpj/mbg-input-cpfcnpj.ts +11 -218
- package/src/helpers/cnpj-validador.ts +38 -0
- package/src/helpers/mbg-document-mask-directive.ts +160 -0
package/package.json
CHANGED
|
@@ -1,82 +1,76 @@
|
|
|
1
1
|
import './mbg-input-cnpj.scss'
|
|
2
2
|
import template from './mbg-input-cnpj.html'
|
|
3
|
+
import {
|
|
4
|
+
normalizeAlphanumericCnpj,
|
|
5
|
+
formatAlphanumericCnpj,
|
|
6
|
+
isValidAlphanumericCnpj,
|
|
7
|
+
mbgDocumentMaskDirective,
|
|
8
|
+
} from '../../helpers/mbg-document-mask-directive'
|
|
9
|
+
import { isValidCnpj } from '../../helpers/cnpj-validador'
|
|
3
10
|
|
|
4
11
|
class MbgInputCnpjController {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
private ngChange
|
|
13
|
+
private ngModel
|
|
14
|
+
private ngRequired
|
|
15
|
+
private ngDisabled
|
|
16
|
+
private props
|
|
17
|
+
private allowAlphanumeric: boolean
|
|
18
|
+
public valid = true
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.props = {
|
|
20
|
-
placeholder: $attrs.placeholder || '',
|
|
21
|
-
}
|
|
20
|
+
constructor(
|
|
21
|
+
public $scope,
|
|
22
|
+
public $element,
|
|
23
|
+
public $attrs,
|
|
24
|
+
) {
|
|
25
|
+
if ($attrs.ngRequired === '') {
|
|
26
|
+
this.ngRequired = true
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this.valid = this.validaCnpj(evt.$event.target.value)
|
|
28
|
+
if ($attrs.ngDisabled === '') {
|
|
29
|
+
this.ngDisabled = true
|
|
26
30
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let original = value
|
|
30
|
-
let firstNumbers = value.substr(0, 12)
|
|
31
|
-
let firstCalc = this.CalcDigits(firstNumbers, 5)
|
|
32
|
-
let secondCalc = this.CalcDigits(firstCalc, 6)
|
|
33
|
-
if (secondCalc === original) {
|
|
34
|
-
return true
|
|
35
|
-
}
|
|
36
|
-
return false
|
|
31
|
+
this.props = {
|
|
32
|
+
placeholder: $attrs.placeholder || '',
|
|
37
33
|
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
$onInit() {
|
|
37
|
+
this.allowAlphanumeric = this.allowAlphanumeric === true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ngBlur(evt) {
|
|
41
|
+
const raw = evt.$event.target.value
|
|
42
|
+
const clean = normalizeAlphanumericCnpj(raw)
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
positions--
|
|
44
|
-
if (positions < 2) {
|
|
45
|
-
positions = 9
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
sumDigits = sumDigits % 11
|
|
49
|
-
if (sumDigits < 2) {
|
|
50
|
-
sumDigits = 0
|
|
51
|
-
} else {
|
|
52
|
-
sumDigits = 11 - sumDigits
|
|
53
|
-
}
|
|
54
|
-
let cnpj = digits + sumDigits
|
|
55
|
-
return cnpj
|
|
44
|
+
if (clean.length === 14) {
|
|
45
|
+
this.valid = isValidAlphanumericCnpj(clean)
|
|
46
|
+
} else {
|
|
47
|
+
this.valid = isValidCnpj(clean)
|
|
56
48
|
}
|
|
49
|
+
}
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
51
|
+
onChange() {
|
|
52
|
+
if (this.ngChange) {
|
|
53
|
+
this.ngChange({})
|
|
62
54
|
}
|
|
55
|
+
}
|
|
63
56
|
}
|
|
64
57
|
MbgInputCnpjController.$inject = ['$scope', '$element', '$attrs']
|
|
65
58
|
|
|
66
59
|
const mbgInputCnpj = {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
60
|
+
bindings: {
|
|
61
|
+
ngModel: '=',
|
|
62
|
+
ngChange: '&?',
|
|
63
|
+
ngRequired: '=?',
|
|
64
|
+
ngDisabled: '=?',
|
|
65
|
+
ngBlur: '&?',
|
|
66
|
+
ngFocus: '&?',
|
|
67
|
+
ngKeyup: '&?',
|
|
68
|
+
ngKeypress: '&?',
|
|
69
|
+
ngKeydown: '&?',
|
|
70
|
+
allowAlphanumeric: '<?',
|
|
71
|
+
},
|
|
72
|
+
template,
|
|
73
|
+
controller: MbgInputCnpjController,
|
|
80
74
|
}
|
|
81
75
|
|
|
82
|
-
export { mbgInputCnpj }
|
|
76
|
+
export { mbgInputCnpj, mbgDocumentMaskDirective }
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as angular from 'angular'
|
|
2
|
-
import { mbgInputCpfCnpj,
|
|
2
|
+
import { mbgInputCpfCnpj, mbgDocumentMaskDirective } from './mbg-input-cpfcnpj'
|
|
3
3
|
|
|
4
4
|
const mbgInputCpfCnpjModule = angular
|
|
5
5
|
.module('mbg.components.mbgInputCpfCnpj', [])
|
|
6
6
|
.component('mbgInputCpfCnpj', mbgInputCpfCnpj)
|
|
7
|
-
.directive('mbgAlphanumericCnpjMask',
|
|
7
|
+
.directive('mbgAlphanumericCnpjMask', mbgDocumentMaskDirective)
|
|
8
8
|
.name
|
|
9
9
|
|
|
10
10
|
export { mbgInputCpfCnpjModule }
|
|
@@ -1,210 +1,13 @@
|
|
|
1
1
|
import './mbg-input-cpfcnpj.scss'
|
|
2
2
|
import template from './mbg-input-cpfcnpj.html'
|
|
3
|
+
import { mbgDocumentMaskDirective } from '../../helpers/mbg-document-mask-directive'
|
|
4
|
+
import { isValidCnpj } from '../../helpers/cnpj-validador'
|
|
3
5
|
|
|
4
6
|
// ---------------------------------------------------------------------------
|
|
5
|
-
//
|
|
7
|
+
// Directive: mbg-document-mask-directive
|
|
8
|
+
// (lógica movida para o helper, re-exportada via import acima)
|
|
6
9
|
// ---------------------------------------------------------------------------
|
|
7
10
|
|
|
8
|
-
const CPF_MASK = '###.###.###-##'
|
|
9
|
-
const CNPJ_MASK = '##.###.###/####-##'
|
|
10
|
-
|
|
11
|
-
function cleanValue(value: string): string {
|
|
12
|
-
return (value || '').replace(/[^A-Za-z0-9]/g, '').toUpperCase()
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function hasLetters(value: string): boolean {
|
|
16
|
-
return /[A-Za-z]/.test(value)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function formatCpf(clean: string): string {
|
|
20
|
-
const digits = clean.replace(/[^0-9]/g, '').substring(0, 11)
|
|
21
|
-
const result: string[] = []
|
|
22
|
-
let digitIdx = 0
|
|
23
|
-
|
|
24
|
-
for (let i = 0; i < CPF_MASK.length && digitIdx < digits.length; i++) {
|
|
25
|
-
if (CPF_MASK[i] === '#') {
|
|
26
|
-
result.push(digits[digitIdx])
|
|
27
|
-
digitIdx++
|
|
28
|
-
} else {
|
|
29
|
-
result.push(CPF_MASK[i])
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return result.join('')
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function formatCnpj(clean: string): string {
|
|
37
|
-
const safe = clean.substring(0, 14)
|
|
38
|
-
const result: string[] = []
|
|
39
|
-
let cleanIndex = 0
|
|
40
|
-
|
|
41
|
-
for (let i = 0; i < CNPJ_MASK.length && cleanIndex < safe.length; i++) {
|
|
42
|
-
const maskChar = CNPJ_MASK[i]
|
|
43
|
-
|
|
44
|
-
if (maskChar === '#') {
|
|
45
|
-
const char = safe[cleanIndex]
|
|
46
|
-
const isCheckDigitPosition = cleanIndex >= 12
|
|
47
|
-
|
|
48
|
-
if (isCheckDigitPosition) {
|
|
49
|
-
if (/[0-9]/.test(char)) {
|
|
50
|
-
result.push(char)
|
|
51
|
-
}
|
|
52
|
-
cleanIndex++
|
|
53
|
-
} else {
|
|
54
|
-
if (/[A-Z0-9]/.test(char)) {
|
|
55
|
-
result.push(char)
|
|
56
|
-
}
|
|
57
|
-
cleanIndex++
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
result.push(maskChar)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return result.join('')
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function smartFormat(value: string): string {
|
|
68
|
-
const clean = normalizeForModel(value)
|
|
69
|
-
if (clean.length === 0) return ''
|
|
70
|
-
if (hasLetters(clean)) return formatCnpj(clean)
|
|
71
|
-
if (clean.length <= 11) return formatCpf(clean)
|
|
72
|
-
return formatCnpj(clean)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function normalizeForModel(value: string): string {
|
|
76
|
-
const clean = cleanValue(value)
|
|
77
|
-
if (!clean) return ''
|
|
78
|
-
if (hasLetters(clean)) return clean.substring(0, 14)
|
|
79
|
-
if (clean.length <= 11) return clean
|
|
80
|
-
return clean.substring(0, 14)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function isRepeatedNumeric(value: string): boolean {
|
|
84
|
-
return /^(\d)\1+$/.test(value)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function getDigitValue(char: string): number {
|
|
88
|
-
return char.charCodeAt(0) - 48
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function calculateCpfDigit(base: string, startWeight: number): number {
|
|
92
|
-
let sum = 0
|
|
93
|
-
|
|
94
|
-
for (let i = 0; i < base.length; i++) {
|
|
95
|
-
sum += Number(base[i]) * (startWeight - i)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const remainder = sum % 11
|
|
99
|
-
return remainder < 2 ? 0 : 11 - remainder
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function isValidCpf(value: string): boolean {
|
|
103
|
-
if (!/^\d{11}$/.test(value) || isRepeatedNumeric(value)) return false
|
|
104
|
-
|
|
105
|
-
const firstDigit = calculateCpfDigit(value.slice(0, 9), 10)
|
|
106
|
-
const secondDigit = calculateCpfDigit(`${value.slice(0, 9)}${firstDigit}`, 11)
|
|
107
|
-
return value === `${value.slice(0, 9)}${firstDigit}${secondDigit}`
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function calculateCnpjDigit(base: string, weights: number[]): number {
|
|
111
|
-
let sum = 0
|
|
112
|
-
|
|
113
|
-
for (let i = 0; i < base.length; i++) {
|
|
114
|
-
sum += getDigitValue(base[i]) * weights[i]
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const remainder = sum % 11
|
|
118
|
-
return remainder < 2 ? 0 : 11 - remainder
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function isValidCnpj(value: string): boolean {
|
|
122
|
-
if (!/^[A-Z0-9]{12}[0-9]{2}$/.test(value)) return false
|
|
123
|
-
if (/^\d{14}$/.test(value) && isRepeatedNumeric(value)) return false
|
|
124
|
-
|
|
125
|
-
const base = value.slice(0, 12)
|
|
126
|
-
const firstDigit = calculateCnpjDigit(base, [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2])
|
|
127
|
-
const secondDigit = calculateCnpjDigit(
|
|
128
|
-
`${base}${firstDigit}`,
|
|
129
|
-
[6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2],
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
return value.slice(12) === `${firstDigit}${secondDigit}`
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function isCpfCandidate(value: string): boolean {
|
|
136
|
-
return /^\d*$/.test(value) && value.length <= 11
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function isCnpjCandidate(value: string): boolean {
|
|
140
|
-
return value.length > 11 || hasLetters(value)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function isDocumentValid(value: string): boolean {
|
|
144
|
-
if (!value) return true
|
|
145
|
-
if (isCpfCandidate(value)) {
|
|
146
|
-
if (value.length < 11) return true
|
|
147
|
-
return isValidCpf(value)
|
|
148
|
-
}
|
|
149
|
-
if (isCnpjCandidate(value)) {
|
|
150
|
-
if (value.length < 14) return true
|
|
151
|
-
if (value.length > 14) return false
|
|
152
|
-
return isValidCnpj(value)
|
|
153
|
-
}
|
|
154
|
-
return false
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
// Directive: mbg-alphanumeric-cnpj-mask
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
class MbgAlphanumericCnpjMaskDirective {
|
|
162
|
-
static $inject = []
|
|
163
|
-
|
|
164
|
-
require = 'ngModel'
|
|
165
|
-
restrict = 'A'
|
|
166
|
-
|
|
167
|
-
link = (_scope, _element, _attrs, ngModel) => {
|
|
168
|
-
ngModel.$parsers.push((viewValue: string) => {
|
|
169
|
-
const clean = normalizeForModel(viewValue || '')
|
|
170
|
-
const formatted = smartFormat(clean)
|
|
171
|
-
|
|
172
|
-
if ((viewValue || '') !== formatted) {
|
|
173
|
-
ngModel.$setViewValue(formatted)
|
|
174
|
-
ngModel.$render()
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return clean
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
ngModel.$formatters.push((modelValue: string) => {
|
|
181
|
-
return smartFormat(modelValue || '')
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
ngModel.$validators.cpf = (modelValue: string) => {
|
|
185
|
-
const clean = normalizeForModel(modelValue || '')
|
|
186
|
-
if (!clean || !isCpfCandidate(clean)) return true
|
|
187
|
-
if (clean.length < 11) return true
|
|
188
|
-
return isValidCpf(clean)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
ngModel.$validators.cnpj = (modelValue: string) => {
|
|
192
|
-
const clean = normalizeForModel(modelValue || '')
|
|
193
|
-
if (!clean || !isCnpjCandidate(clean)) return true
|
|
194
|
-
if (clean.length < 14) return true
|
|
195
|
-
if (clean.length > 14) return false
|
|
196
|
-
return isValidCnpj(clean)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
ngModel.$validators.cpfcnpj = (modelValue: string) => {
|
|
200
|
-
const clean = normalizeForModel(modelValue || '')
|
|
201
|
-
return isDocumentValid(clean)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const mbgAlphanumericCnpjMask = () => new MbgAlphanumericCnpjMaskDirective()
|
|
207
|
-
|
|
208
11
|
// ---------------------------------------------------------------------------
|
|
209
12
|
// Component: mbg-input-cpfcnpj
|
|
210
13
|
// ---------------------------------------------------------------------------
|
|
@@ -240,16 +43,13 @@ class MbgInputCpfCnpjController {
|
|
|
240
43
|
}
|
|
241
44
|
|
|
242
45
|
ngBlur(evt) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
this.
|
|
248
|
-
}
|
|
249
|
-
if (this.valid.length === 14) {
|
|
250
|
-
this.validCnpj(this.valid)
|
|
46
|
+
const raw = `${evt.$event.target.value || ''}`.replace(/[^A-Za-z0-9]/g, '').toUpperCase()
|
|
47
|
+
if (raw.length === 11) {
|
|
48
|
+
this.valid = this.validCpf(raw)
|
|
49
|
+
} else if (raw.length === 14) {
|
|
50
|
+
this.valid = isValidCnpj(raw)
|
|
251
51
|
} else {
|
|
252
|
-
|
|
52
|
+
this.valid = false
|
|
253
53
|
}
|
|
254
54
|
}
|
|
255
55
|
|
|
@@ -273,13 +73,6 @@ class MbgInputCpfCnpjController {
|
|
|
273
73
|
return newCpf === value
|
|
274
74
|
}
|
|
275
75
|
|
|
276
|
-
validCnpj(value) {
|
|
277
|
-
const firstNumbers = value.substr(0, 12)
|
|
278
|
-
const firstCalc = this.CalcDigits(firstNumbers, 5)
|
|
279
|
-
const secondCalc = this.CalcDigits(firstCalc, 6)
|
|
280
|
-
return secondCalc === value
|
|
281
|
-
}
|
|
282
|
-
|
|
283
76
|
onChange() {
|
|
284
77
|
if (this.ngChange) this.ngChange({})
|
|
285
78
|
}
|
|
@@ -304,4 +97,4 @@ const mbgInputCpfCnpj = {
|
|
|
304
97
|
controller: MbgInputCpfCnpjController,
|
|
305
98
|
}
|
|
306
99
|
|
|
307
|
-
export { mbgInputCpfCnpj,
|
|
100
|
+
export { mbgInputCpfCnpj, mbgDocumentMaskDirective }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Single source of truth for CNPJ normalization and validation.
|
|
2
|
+
|
|
3
|
+
// Strips punctuation/spaces, uppercases letters, truncates to 14 chars.
|
|
4
|
+
// Preserves letters — safe for both numeric and alphanumeric CNPJs.
|
|
5
|
+
export function normalizeCnpj(value: string): string {
|
|
6
|
+
const clean = `${value || ''}`.replace(/[^A-Za-z0-9]/g, '').toUpperCase()
|
|
7
|
+
return clean.substring(0, 14)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const WEIGHTS_D1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
|
|
11
|
+
const WEIGHTS_D2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
|
|
12
|
+
|
|
13
|
+
function charToValue(char: string): number {
|
|
14
|
+
return char.charCodeAt(0) - 48
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function calcDigit(base: string, weights: number[]): number {
|
|
18
|
+
let sum = 0
|
|
19
|
+
for (let i = 0; i < base.length; i++) {
|
|
20
|
+
sum += charToValue(base[i]) * weights[i]
|
|
21
|
+
}
|
|
22
|
+
const r = sum % 11
|
|
23
|
+
return r < 2 ? 0 : 11 - r
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Accepts raw or pre-normalized input (with or without punctuation/mask).
|
|
27
|
+
// Validates numeric and alphanumeric CNPJs (Receita Federal 2026).
|
|
28
|
+
export function isValidCnpj(value: string): boolean {
|
|
29
|
+
const cnpj = normalizeCnpj(value)
|
|
30
|
+
|
|
31
|
+
if (!/^[A-Z0-9]{12}[0-9]{2}$/.test(cnpj)) return false
|
|
32
|
+
if (/^\d{14}$/.test(cnpj) && /^(\d)\1+$/.test(cnpj)) return false
|
|
33
|
+
|
|
34
|
+
const base = cnpj.slice(0, 12)
|
|
35
|
+
const d1 = calcDigit(base, WEIGHTS_D1)
|
|
36
|
+
const d2 = calcDigit(`${base}${d1}`, WEIGHTS_D2)
|
|
37
|
+
return cnpj.slice(12) === `${d1}${d2}`
|
|
38
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Shared utilities: Alphanumeric CNPJ mask, formatting and validation
|
|
3
|
+
// Used by: mbg-input-cpfcnpj, mbg-input-cnpj
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
import { isValidCnpj, normalizeCnpj } from '@kigi/components/src/helpers/cnpj-validador'
|
|
7
|
+
export { isValidCnpj as isValidAlphanumericCnpj }
|
|
8
|
+
export { normalizeCnpj as normalizeAlphanumericCnpj }
|
|
9
|
+
|
|
10
|
+
const CNPJ_MASK = '##.###.###/####-##'
|
|
11
|
+
const CPF_MASK = '###.###.###-##'
|
|
12
|
+
|
|
13
|
+
export function hasLetters(value: string): boolean {
|
|
14
|
+
return /[A-Za-z]/.test(value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function formatCpf(clean: string): string {
|
|
18
|
+
const digits = clean.replace(/[^0-9]/g, '').substring(0, 11)
|
|
19
|
+
const result: string[] = []
|
|
20
|
+
let digitIdx = 0
|
|
21
|
+
for (let i = 0; i < CPF_MASK.length && digitIdx < digits.length; i++) {
|
|
22
|
+
if (CPF_MASK[i] === '#') result.push(digits[digitIdx++])
|
|
23
|
+
else result.push(CPF_MASK[i])
|
|
24
|
+
}
|
|
25
|
+
return result.join('')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function smartFormat(clean: string): string {
|
|
29
|
+
if (!clean) return ''
|
|
30
|
+
if (hasLetters(clean)) return formatAlphanumericCnpj(clean)
|
|
31
|
+
if (clean.length <= 11) return formatCpf(clean)
|
|
32
|
+
return formatAlphanumericCnpj(clean)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatAlphanumericCnpj(clean: string): string {
|
|
36
|
+
const safe = clean.substring(0, 14)
|
|
37
|
+
const result: string[] = []
|
|
38
|
+
let cleanIndex = 0
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < CNPJ_MASK.length && cleanIndex < safe.length; i++) {
|
|
41
|
+
const maskChar = CNPJ_MASK[i]
|
|
42
|
+
|
|
43
|
+
if (maskChar === '#') {
|
|
44
|
+
const char = safe[cleanIndex]
|
|
45
|
+
const isCheckDigitPosition = cleanIndex >= 12
|
|
46
|
+
|
|
47
|
+
if (isCheckDigitPosition) {
|
|
48
|
+
if (/[0-9]/.test(char)) result.push(char)
|
|
49
|
+
cleanIndex++
|
|
50
|
+
} else {
|
|
51
|
+
if (/[A-Z0-9]/.test(char)) result.push(char)
|
|
52
|
+
cleanIndex++
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
result.push(maskChar)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result.join('')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isCnpjCandidate(value: string): boolean {
|
|
63
|
+
return value.length > 11 || hasLetters(value)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isCpfCandidate(value: string): boolean {
|
|
67
|
+
return /^\d*$/.test(value) && value.length <= 11
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isRepeatedNumeric(value: string): boolean {
|
|
71
|
+
return /^(\d)\1+$/.test(value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function calculateCpfDigit(base: string, startWeight: number): number {
|
|
75
|
+
let sum = 0
|
|
76
|
+
for (let i = 0; i < base.length; i++) {
|
|
77
|
+
sum += Number(base[i]) * (startWeight - i)
|
|
78
|
+
}
|
|
79
|
+
const remainder = sum % 11
|
|
80
|
+
return remainder < 2 ? 0 : 11 - remainder
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isValidCpf(value: string): boolean {
|
|
84
|
+
if (!/^\d{11}$/.test(value) || isRepeatedNumeric(value)) return false
|
|
85
|
+
const firstDigit = calculateCpfDigit(value.slice(0, 9), 10)
|
|
86
|
+
const secondDigit = calculateCpfDigit(`${value.slice(0, 9)}${firstDigit}`, 11)
|
|
87
|
+
return value === `${value.slice(0, 9)}${firstDigit}${secondDigit}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isDocumentValid(value: string): boolean {
|
|
91
|
+
if (!value) return true
|
|
92
|
+
if (isCpfCandidate(value)) {
|
|
93
|
+
if (value.length < 11) return true
|
|
94
|
+
return isValidCpf(value)
|
|
95
|
+
}
|
|
96
|
+
if (isCnpjCandidate(value)) {
|
|
97
|
+
if (value.length < 14) {
|
|
98
|
+
if (hasLetters(value)) return false
|
|
99
|
+
return true
|
|
100
|
+
}
|
|
101
|
+
if (value.length > 14) return false
|
|
102
|
+
return isValidCnpj(value)
|
|
103
|
+
}
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// AngularJS directive factory — reusable in any module
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
class MbgDocumentMaskDirective {
|
|
112
|
+
static $inject = []
|
|
113
|
+
|
|
114
|
+
require = 'ngModel'
|
|
115
|
+
restrict = 'A'
|
|
116
|
+
|
|
117
|
+
link = (_scope, _element, _attrs, ngModel) => {
|
|
118
|
+
ngModel.$parsers.push((viewValue: string) => {
|
|
119
|
+
const clean = normalizeCnpj(viewValue || '')
|
|
120
|
+
const formatted = smartFormat(clean)
|
|
121
|
+
|
|
122
|
+
if ((viewValue || '') !== formatted) {
|
|
123
|
+
ngModel.$setViewValue(formatted)
|
|
124
|
+
ngModel.$render()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return clean
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
ngModel.$formatters.push((modelValue: string) => {
|
|
131
|
+
return smartFormat(normalizeCnpj(modelValue || ''))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
ngModel.$validators.cpf = (modelValue: string) => {
|
|
135
|
+
const clean = normalizeCnpj(modelValue || '')
|
|
136
|
+
if (!clean || !isCpfCandidate(clean)) return true
|
|
137
|
+
if (clean.length < 11) return true
|
|
138
|
+
return isValidCpf(clean)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
ngModel.$validators.cnpj = (modelValue: string) => {
|
|
142
|
+
const clean = normalizeCnpj(modelValue || '')
|
|
143
|
+
if (!clean || !isCnpjCandidate(clean)) return true
|
|
144
|
+
if (clean.length < 14) {
|
|
145
|
+
if (hasLetters(clean)) return false
|
|
146
|
+
if (clean.length >= 12 && /[A-Z]/.test(clean.slice(12))) return false
|
|
147
|
+
return true
|
|
148
|
+
}
|
|
149
|
+
if (clean.length > 14) return false
|
|
150
|
+
return isValidCnpj(clean)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ngModel.$validators.cpfcnpj = (modelValue: string) => {
|
|
154
|
+
const clean = normalizeCnpj(modelValue || '')
|
|
155
|
+
return isDocumentValid(clean)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const mbgDocumentMaskDirective = () => new MbgDocumentMaskDirective()
|