tax-document-input
Version:
A vanilla JavaScript plugin for automatic formatting of tax documents from different countries (CPF, CNPJ, NIF, NIPC, SSN, EIN)
1,414 lines (1,218 loc) • 91.4 kB
JavaScript
/*!
* Tax Document Input Plugin v2.0.0
* Plugin JavaScript para formatação de documentos fiscais
*
* Copyright (c) 2025 Roni Sommerfeld
* Released under the MIT License
*
* Date: 2025-07-05
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.TaxDocumentInput = {}));
})(this, (function (exports) { 'use strict';
/**
* Validator - Tax document validation system
* Manages validation rules by country and provides a unified validation interface
* @version 1.0.0
* @license MIT
* @author Roni Sommerfeld
* @module Validator
*/
class Validator {
constructor() {
this.rules = new Map();
this.loadRules();
}
/**
* Loads validation rules by country
* @description Initializes the validator and prepares for rule registration
* @private
* @method loadRules
* @version 1.0.0
* @returns {void}
*/
loadRules() {
}
/**
* Registers validation rules for a country
* @description Stores validation functions for a specific country's document types
* @param {string} countryCode - ISO2 country code
* @param {Object} rules - Object containing validation functions
* @method registerRules
* @version 1.0.0
* @returns {void}
*/
registerRules(countryCode, rules) {
this.rules.set(countryCode.toLowerCase(), rules);
}
/**
* Validates a tax document
* @description Performs validation using the appropriate country-specific validator
* @param {string} document - Document without formatting (numbers only)
* @param {string} countryCode - ISO2 country code
* @param {string} documentType - Document type (cpf, cnpj, nif, etc.)
* @method validate
* @version 1.0.0
* @returns {Object} Validation result with isValid, error, and details
*/
validate(document, countryCode, documentType) {
const rules = this.rules.get(countryCode.toLowerCase());
if (!rules) {
return {
isValid: false,
error: `Regras de validação não encontradas para o país: ${countryCode}`,
details: null
};
}
const validator = rules[documentType];
if (!validator || typeof validator !== 'function') {
return {
isValid: false,
error: `Validador não encontrado para o tipo: ${documentType}`,
details: null
};
}
try {
const result = validator(document);
return {
isValid: result.isValid,
error: result.error || null,
details: result.details || null,
documentType: documentType,
countryCode: countryCode
};
} catch (error) {
return {
isValid: false,
error: `Erro durante validação: ${error.message}`,
details: null
};
}
}
/**
* Checks if a country has registered validation rules
* @description Verifies if validation rules exist for the specified country
* @param {string} countryCode - ISO2 country code
* @method hasRules
* @version 1.0.0
* @returns {boolean} True if rules exist for the country
*/
hasRules(countryCode) {
return this.rules.has(countryCode.toLowerCase());
}
/**
* Lists all countries with registered rules
* @description Returns an array of country codes that have validation rules
* @method getAvailableCountries
* @version 1.0.0
* @returns {Array} Array of country codes
*/
getAvailableCountries() {
return Array.from(this.rules.keys());
}
/**
* Gets supported document types for a country
* @description Returns the document types that can be validated for a specific country
* @param {string} countryCode - ISO2 country code
* @method getSupportedDocuments
* @version 1.0.0
* @returns {Array} Array of document type strings
*/
getSupportedDocuments(countryCode) {
const rules = this.rules.get(countryCode.toLowerCase());
return rules ? Object.keys(rules) : [];
}
}
const ValidatorInstance = new Validator();
if (typeof window !== 'undefined') {
window.TaxDocumentValidator = ValidatorInstance;
}
/**
* BrazilRules - Brazilian tax document validation rules
* Provides validation algorithms for CPF and CNPJ with correct check digit calculations
* @version 1.0.0
* @license MIT
* @author Roni Sommerfeld
* @module BrazilRules
* @requires ValidatorInstance
*/
const BrazilRules = {
/**
* Validates CPF (Cadastro de Pessoas Físicas)
* @description Validates Brazilian individual taxpayer registry using correct algorithm
* @param {string} cpf - CPF with numbers only
* @returns {Object} Validation result with isValid, error and details
* @version 1.0.0
*/
cpf: function(cpf) {
cpf = cpf.replace(/\D/g, '');
if (cpf.length !== 11) {
return {
isValid: false,
error: 'CPF deve conter exatamente 11 dígitos',
details: { length: cpf.length, expected: 11 }
};
}
if (/^(\d)\1{10}$/.test(cpf)) {
return {
isValid: false,
error: 'CPF não pode ter todos os dígitos iguais',
details: { pattern: 'repeated_digits' }
};
}
let sum = 0;
for (let i = 0; i < 9; i++) {
sum += parseInt(cpf.charAt(i)) * (10 - i);
}
let remainder = sum % 11;
let digit1 = remainder < 2 ? 0 : 11 - remainder;
if (digit1 !== parseInt(cpf.charAt(9))) {
return {
isValid: false,
error: 'Primeiro dígito verificador inválido',
details: {
calculated: digit1,
provided: parseInt(cpf.charAt(9)),
sum: sum,
remainder: remainder
}
};
}
sum = 0;
for (let i = 0; i < 10; i++) {
sum += parseInt(cpf.charAt(i)) * (11 - i);
}
remainder = sum % 11;
let digit2 = remainder < 2 ? 0 : 11 - remainder;
if (digit2 !== parseInt(cpf.charAt(10))) {
return {
isValid: false,
error: 'Segundo dígito verificador inválido',
details: {
calculated: digit2,
provided: parseInt(cpf.charAt(10)),
sum: sum,
remainder: remainder
}
};
}
return {
isValid: true,
error: null,
details: {
formatted: `${cpf.slice(0,3)}.${cpf.slice(3,6)}.${cpf.slice(6,9)}-${cpf.slice(9,11)}`,
type: 'personal',
country: 'BR'
}
};
},
/**
* Validates CNPJ (Cadastro Nacional da Pessoa Jurídica)
* @description Validates Brazilian company taxpayer registry using correct algorithm
* @param {string} cnpj - CNPJ with numbers only
* @returns {Object} Validation result with isValid, error and details
* @version 1.0.0
*/
cnpj: function(cnpj) {
cnpj = cnpj.replace(/\D/g, '');
if (cnpj.length !== 14) {
return {
isValid: false,
error: 'CNPJ deve conter exatamente 14 dígitos',
details: { length: cnpj.length, expected: 14 }
};
}
if (/^(\d)\1{13}$/.test(cnpj)) {
return {
isValid: false,
error: 'CNPJ não pode ter todos os dígitos iguais',
details: { pattern: 'repeated_digits' }
};
}
const weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
let sum = 0;
for (let i = 0; i < 12; i++) {
sum += parseInt(cnpj.charAt(i)) * weights1[i];
}
let remainder = sum % 11;
let digit1 = remainder < 2 ? 0 : 11 - remainder;
if (digit1 !== parseInt(cnpj.charAt(12))) {
return {
isValid: false,
error: 'Primeiro dígito verificador inválido',
details: {
calculated: digit1,
provided: parseInt(cnpj.charAt(12)),
sum: sum,
remainder: remainder
}
};
}
const weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
sum = 0;
for (let i = 0; i < 13; i++) {
sum += parseInt(cnpj.charAt(i)) * weights2[i];
}
remainder = sum % 11;
let digit2 = remainder < 2 ? 0 : 11 - remainder;
if (digit2 !== parseInt(cnpj.charAt(13))) {
return {
isValid: false,
error: 'Segundo dígito verificador inválido',
details: {
calculated: digit2,
provided: parseInt(cnpj.charAt(13)),
sum: sum,
remainder: remainder
}
};
}
return {
isValid: true,
error: null,
details: {
formatted: `${cnpj.slice(0,2)}.${cnpj.slice(2,5)}.${cnpj.slice(5,8)}/${cnpj.slice(8,12)}-${cnpj.slice(12,14)}`,
type: 'company',
country: 'BR'
}
};
}
};
ValidatorInstance.registerRules('br', BrazilRules);
if (typeof window !== 'undefined' && window.TaxDocumentValidator) {
window.TaxDocumentValidator.registerRules('br', BrazilRules);
}
/**
* PortugalRules - Portuguese tax document validation rules
* Provides validation algorithms for NIF and NIPC with correct check digit calculations
* @version 1.0.0
* @license MIT
* @author Roni Sommerfeld
* @module PortugalRules
* @requires ValidatorInstance
*/
const PortugalRules = {
/**
* Validates NIF (Número de Identificação Fiscal)
* @description Validates Portuguese taxpayer identification number for all entity types
* @param {string} nif - NIF with numbers only
* @returns {Object} Validation result with isValid, error and details
* @version 1.0.1
*/
nif: function(nif) {
nif = nif.replace(/\D/g, '');
if (nif.length !== 9) {
return {
isValid: false,
error: 'NIF must contain exactly 9 digits',
details: { length: nif.length, expected: 9 }
};
}
if (/^(\d)\1{8}$/.test(nif)) {
return {
isValid: false,
error: 'NIF cannot have all digits the same',
details: { pattern: 'repeated_digits' }
};
}
const validFirstDigits = ['1', '2', '3', '5', '6', '7', '8', '9'];
if (!validFirstDigits.includes(nif.charAt(0))) {
return {
isValid: false,
error: 'NIF must start with 1, 2, 3, 5, 6, 7, 8 or 9',
details: { firstDigit: nif.charAt(0), validFirstDigits }
};
}
const weights = [9, 8, 7, 6, 5, 4, 3, 2];
let sum = 0;
for (let i = 0; i < 8; i++) {
sum += parseInt(nif.charAt(i)) * weights[i];
}
const remainder = sum % 11;
let checkDigit = 11 - remainder;
if (remainder < 2) {
checkDigit = 0;
}
if (checkDigit !== parseInt(nif.charAt(8))) {
return {
isValid: false,
error: 'Invalid check digit',
details: { calculated: checkDigit, provided: parseInt(nif.charAt(8)) }
};
}
const entityType = this.getEntityType(nif.charAt(0));
return {
isValid: true,
error: null,
details: {
formatted: `${nif.slice(0,3)} ${nif.slice(3,6)} ${nif.slice(6,9)}`,
type: entityType,
country: 'PT'
}
};
},
/**
* Validates NIPC (Número de Identificação de Pessoa Coletiva) - Company
* @description Validates Portuguese company taxpayer identification number
* @param {string} nipc - NIPC with numbers only
* @returns {Object} Validation result with isValid, error and details
* @version 1.0.0
*/
nipc: function(nipc) {
nipc = nipc.replace(/\D/g, '');
if (nipc.length !== 9) {
return {
isValid: false,
error: 'NIPC must contain exactly 9 digits',
details: { length: nipc.length, expected: 9 }
};
}
if (/^(\d)\1{8}$/.test(nipc)) {
return {
isValid: false,
error: 'NIPC cannot have all digits the same',
details: { pattern: 'repeated_digits' }
};
}
const validFirstDigits = ['5', '6', '7', '8', '9'];
if (!validFirstDigits.includes(nipc.charAt(0))) {
return {
isValid: false,
error: 'NIPC must start with 5, 6, 7, 8 or 9',
details: { firstDigit: nipc.charAt(0), validFirstDigits }
};
}
const weights = [9, 8, 7, 6, 5, 4, 3, 2];
let sum = 0;
for (let i = 0; i < 8; i++) {
sum += parseInt(nipc.charAt(i)) * weights[i];
}
const remainder = sum % 11;
let checkDigit = 11 - remainder;
if (remainder < 2) {
checkDigit = 0;
}
if (checkDigit !== parseInt(nipc.charAt(8))) {
return {
isValid: false,
error: 'Invalid check digit',
details: { calculated: checkDigit, provided: parseInt(nipc.charAt(8)) }
};
}
return {
isValid: true,
error: null,
details: {
formatted: `${nipc.slice(0,3)} ${nipc.slice(3,6)} ${nipc.slice(6,9)}`,
type: 'company',
country: 'PT'
}
};
},
/**
* Determines entity type based on first digit
* @description Maps first digit to entity type for Portuguese NIFs
* @param {string} firstDigit - First digit of the NIF
* @returns {string} Entity type description
* @version 1.0.1
*/
getEntityType: function(firstDigit) {
const entityTypes = {
'1': 'personal',
'2': 'personal',
'3': 'personal',
'5': 'public_entity',
'6': 'non_profit',
'7': 'other_entity',
'8': 'other_entity',
'9': 'other_entity'
};
return entityTypes[firstDigit] || 'unknown';
}
};
ValidatorInstance.registerRules('pt', PortugalRules);
if (typeof window !== 'undefined' && window.TaxDocumentValidator) {
window.TaxDocumentValidator.registerRules('pt', PortugalRules);
}
/**
* USARules - American tax document validation rules
* Provides validation algorithms for SSN and EIN with correct format validation
* @version 1.0.0
* @license MIT
* @author Roni Sommerfeld
* @module USARules
* @requires ValidatorInstance
*/
const USARules = {
/**
* Validates SSN (Social Security Number) - Individual
* @description Validates American individual taxpayer identification number
* @param {string} ssn - SSN with numbers only
* @returns {Object} Validation result with isValid, error and details
* @version 1.0.0
*/
ssn: function(ssn) {
ssn = ssn.replace(/\D/g, '');
if (ssn.length !== 9) {
return {
isValid: false,
error: 'SSN must contain exactly 9 digits',
details: { length: ssn.length, expected: 9 }
};
}
if (/^(\d)\1{8}$/.test(ssn)) {
return {
isValid: false,
error: 'SSN cannot have all digits the same',
details: { pattern: 'repeated_digits' }
};
}
const area = ssn.slice(0, 3);
const group = ssn.slice(3, 5);
const serial = ssn.slice(5, 9);
if (area === '000') {
return {
isValid: false,
error: 'SSN area cannot be 000',
details: { area, issue: 'invalid_area_000' }
};
}
if (area === '666') {
return {
isValid: false,
error: '',
details: { area, issue: 'invalid_area_666' }
};
}
if (area.charAt(0) === '9') {
return {
isValid: false,
error: 'SSN area cannot start with 9',
details: { area, issue: 'invalid_area_starts_with_9' }
};
}
if (group === '00') {
return {
isValid: false,
error: 'SSN group cannot be 00',
details: { group, issue: 'invalid_group_00' }
};
}
if (serial === '0000') {
return {
isValid: false,
error: 'SSN serial number cannot be 0000',
details: { serial, issue: 'invalid_serial_0000' }
};
}
return {
isValid: true,
error: null,
details: {
formatted: `${area}-${group}-${serial}`,
type: 'personal',
country: 'US',
parts: { area, group, serial }
}
};
},
/**
* Validates EIN (Employer Identification Number) - Company
* @description Validates American company taxpayer identification number
* @param {string} ein - EIN with numbers only
* @returns {Object} Validation result with isValid, error and details
* @version 1.0.0
*/
ein: function(ein) {
ein = ein.replace(/\D/g, '');
if (ein.length !== 9) {
return {
isValid: false,
error: 'EIN must contain exactly 9 digits',
details: { length: ein.length, expected: 9 }
};
}
if (/^(\d)\1{8}$/.test(ein)) {
return {
isValid: false,
error: 'EIN cannot have all digits the same',
details: { pattern: 'repeated_digits' }
};
}
const prefix = ein.slice(0, 2);
const suffix = ein.slice(2, 9);
const validPrefixes = [
'01', '02', '03', '04', '05', '06', '10', '11', '12', '13', '14', '15', '16',
'20', '21', '22', '23', '24', '25', '26', '27',
'30', '31', '32', '33', '34', '35', '36', '37', '38', '39',
'40', '41', '42', '43', '44', '45', '46', '47', '48',
'50', '51', '52', '53', '54', '55', '56', '57', '58', '59',
'60', '61', '62', '63', '64', '65', '66', '67', '68',
'71', '72', '73', '74', '75', '76', '77',
'80', '81', '82', '83', '84', '85', '86', '87', '88',
'90', '91', '92', '93', '94', '95', '96', '98', '99'
];
if (!validPrefixes.includes(prefix)) {
return {
isValid: false,
error: 'Invalid EIN prefix',
details: { prefix, issue: 'invalid_prefix' }
};
}
if (suffix === '0000000') {
return {
isValid: false,
error: 'EIN suffix cannot be 0000000',
details: { suffix, issue: 'invalid_suffix_all_zeros' }
};
}
return {
isValid: true,
error: null,
details: {
formatted: `${prefix}-${suffix}`,
type: 'company',
country: 'US',
parts: { prefix, suffix }
}
};
}
};
ValidatorInstance.registerRules('us', USARules);
if (typeof window !== 'undefined' && window.TaxDocumentValidator) {
window.TaxDocumentValidator.registerRules('us', USARules);
}
var CountriesData = {
br: {
name: 'Brasil',
iso2: 'br',
flag: 'https://flagcdn.com/w20/br.png',
documents: {
cpf: {
length: 11,
mask: 'XXX.XXX.XXX-XX',
type: 'personal',
priority: 1
},
cnpj: {
length: 14,
mask: 'XX.XXX.XXX/XXXX-XX',
type: 'company',
priority: 2
}
}
},
pt: {
name: 'Portugal',
iso2: 'pt',
flag: 'https://flagcdn.com/w20/pt.png',
documents: {
nif: {
length: 9,
mask: 'XXX XXX XXX',
type: 'personal',
priority: 1
},
nipc: {
length: 9,
mask: 'XXX XXX XXX',
type: 'company',
priority: 2
}
}
},
us: {
name: 'United States',
iso2: 'us',
flag: 'https://flagcdn.com/w20/us.png',
documents: {
ssn: {
length: 9,
mask: 'XXX-XX-XXXX',
type: 'personal',
priority: 1
},
ein: {
length: 9,
mask: 'XX-XXXXXXX',
type: 'company',
priority: 2
}
}
}
};
const debug = (...args) => {
// console.log(...args);
};
/**
* CountryManager - Versão Robusta Anti-Conflito
* Gerencia seleção de países e dropdown
*/
class CountryManager {
constructor(countries, onlyCountries = []) {
this.countries = countries;
this.onlyCountries = onlyCountries;
this.selectedCountry = 'br';
this.domManager = null;
this.isDropdownVisible = false;
this.debugMode = true; // Ativar debug por padrão
}
/**
* Define o DOMManager para manipulação do dropdown
*/
setDOMManager(domManager) {
this.domManager = domManager;
}
/**
* Inicializa o gerenciador de países
*/
initialize(defaultCountry) {
this.selectedCountry = defaultCountry;
this.populateCountries();
this.updateSelectedCountry();
this.setupEventListeners();
if (this.debugMode) {
debug('CountryManager inicializado:', {
defaultCountry,
availableCountries: this.onlyCountries.length > 0 ? this.onlyCountries : Object.keys(this.countries),
domManagerReady: !!this.domManager
});
}
}
/**
* Popula o dropdown com países disponíveis
*/
populateCountries() {
const availableCountries = this.onlyCountries.length > 0
? this.onlyCountries
: Object.keys(this.countries);
this.domManager.populateCountries(availableCountries, (countryCode) => {
this.selectCountry(countryCode);
});
// Verificação com timeout maior
setTimeout(() => {
const items = this.domManager.dropdown.querySelectorAll('.tax-document-input__dropdown-item');
debug('Verificação pós-população: itens criados:', items.length);
if (items.length === 0) {
console.error('ERRO: Dropdown não foi populado!');
debug('Países registrados:', Object.keys(this.countries));
this.domManager.populateCountries(availableCountries, (countryCode) => {
this.selectCountry(countryCode);
});
}
}, 200);
}
/**
* Atualiza o país selecionado na interface
*/
updateSelectedCountry() {
const country = this.countries[this.selectedCountry];
this.domManager.updateSelectedCountry(country);
}
/**
* Seleciona um país
*/
selectCountry(countryCode) {
const previousCountry = this.selectedCountry;
this.selectedCountry = countryCode;
this.updateSelectedCountry();
this.hideDropdown();
// Notificar sobre mudança de país
this.onCountryChange?.(countryCode, previousCountry);
if (this.debugMode) ;
}
/**
* Configura os event listeners - Versão Robusta
*/
setupEventListeners() {
if (!this.domManager || !this.domManager.countryButton || !this.domManager.dropdown) {
console.error('DOMManager não está configurado corretamente');
return;
}
// Toggle dropdown com debug
this.domManager.countryButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
debug('Estado antes do toggle:', {
isVisible: this.isDropdownVisible,
display: this.domManager.dropdown.style.display,
computedDisplay: getComputedStyle(this.domManager.dropdown).display
});
this.toggleDropdown();
});
// Fechar dropdown ao clicar fora - com debug
document.addEventListener('click', (e) => {
if (this.isDropdownVisible) {
const isInsideWrapper = this.domManager.wrapper.contains(e.target);
const isInsideDropdown = this.domManager.dropdown.contains(e.target);
if (!isInsideWrapper && !isInsideDropdown) {
if (this.debugMode) {
debug('Clicou fora, fechando dropdown', {
target: e.target,
isInsideWrapper,
isInsideDropdown
});
}
this.hideDropdown();
}
}
});
// Prevenir que cliques dentro do dropdown fechem ele
this.domManager.dropdown.addEventListener('click', (e) => {
e.stopPropagation();
e.stopImmediatePropagation();
if (this.debugMode) {
debug('Clique dentro do dropdown', e.target);
}
});
// Reposicionar dropdown ao redimensionar ou scroll
const repositionHandler = () => {
if (this.isDropdownVisible) {
this.positionDropdown();
}
};
window.addEventListener('resize', repositionHandler);
window.addEventListener('scroll', repositionHandler, true);
// Fechar dropdown com ESC
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isDropdownVisible) {
this.hideDropdown();
}
});
if (this.debugMode) ;
}
/**
* Alterna a exibição do dropdown
*/
toggleDropdown() {
if (!this.domManager || !this.domManager.dropdown) {
console.error('Dropdown não encontrado');
return;
}
debug('Toggle dropdown - isVisible:', this.isDropdownVisible);
if (this.isDropdownVisible) {
this.hideDropdown();
} else {
this.showDropdown();
}
}
/**
* Exibe o dropdown - Versão Robusta
*/
showDropdown() {
if (!this.domManager || !this.domManager.dropdown) {
console.error('Elementos necessários não encontrados para showDropdown');
return;
}
try {
// 1. Primeiro, posicionar o dropdown
this.positionDropdown();
// 2. Aplicar estilos inline críticos
this.domManager.forceShowDropdown();
// 3. Usar atributo para CSS específico
this.domManager.dropdown.setAttribute('data-visible', 'true');
// 4. Forçar display via style
this.domManager.dropdown.style.display = 'block';
this.domManager.dropdown.style.visibility = 'visible';
this.domManager.dropdown.style.opacity = '1';
// 5. Forçar reflow múltiplas vezes
this.domManager.dropdown.offsetHeight;
this.domManager.dropdown.getBoundingClientRect();
// 6. Atualizar estado
this.isDropdownVisible = true;
debug('Dropdown mostrado com sucesso');
// 7. Debug detalhado após um frame
requestAnimationFrame(() => {
const rect = this.domManager.dropdown.getBoundingClientRect();
const computed = getComputedStyle(this.domManager.dropdown);
const debugInfo = {
display: computed.display,
position: computed.position,
top: computed.top,
left: computed.left,
zIndex: computed.zIndex,
visibility: computed.visibility,
opacity: computed.opacity,
width: rect.width,
height: rect.height,
inViewport: this.isInViewport(rect),
hasItems: this.domManager.dropdown.children.length,
styleDisplay: this.domManager.dropdown.style.display,
dataVisible: this.domManager.dropdown.getAttribute('data-visible')
};
debug('Estado final do dropdown:', debugInfo);
// Se ainda não está visível, forçar mais
if (computed.display === 'none' || rect.width === 0) {
console.warn('Dropdown ainda não visível, aplicando correção de emergência');
this.emergencyShowFix();
} else {
// Destacar visualmente se estiver funcionando
this.highlightDropdown();
}
});
} catch (error) {
console.error('Erro em showDropdown:', error);
this.emergencyShowFix();
}
}
/**
* Correção de emergência quando dropdown não aparece
*/
emergencyShowFix() {
const emergency = document.createElement('div');
emergency.id = 'emergency-dropdown-' + Date.now();
emergency.innerHTML = this.domManager.dropdown.innerHTML;
// Estilos de emergência
Object.assign(emergency.style, {
position: 'fixed',
top: '100px',
left: '100px',
width: '250px',
maxHeight: '200px',
backgroundColor: '#ffcccc',
border: '3px solid red',
zIndex: '2147483647',
padding: '10px',
borderRadius: '4px',
boxShadow: '0 4px 20px rgba(0,0,0,0.5)',
fontSize: '14px',
fontFamily: 'Arial, sans-serif',
overflowY: 'auto'
});
// Adicionar mensagem de debug
const debugMsg = document.createElement('div');
debugMsg.style.cssText = 'background: yellow; padding: 5px; margin-bottom: 5px; font-size: 12px;';
debugMsg.textContent = 'DROPDOWN DE EMERGÊNCIA - Há conflito de CSS!';
emergency.insertBefore(debugMsg, emergency.firstChild);
document.body.appendChild(emergency);
// Remover após 5 segundos
setTimeout(() => {
if (document.body.contains(emergency)) {
document.body.removeChild(emergency);
}
}, 5000);
}
/**
* Destaca o dropdown visualmente para confirmar que está funcionando
*/
highlightDropdown() {
const originalBg = this.domManager.dropdown.style.backgroundColor;
const originalBorder = this.domManager.dropdown.style.border;
this.domManager.dropdown.style.backgroundColor = '#ffffcc';
this.domManager.dropdown.style.border = '3px solid green';
setTimeout(() => {
this.domManager.dropdown.style.backgroundColor = originalBg || 'white';
this.domManager.dropdown.style.border = originalBorder || '1px solid #ccc';
}, 1500);
}
/**
* Esconde o dropdown
*/
hideDropdown() {
if (!this.domManager || !this.domManager.dropdown) return;
this.domManager.dropdown.style.display = 'none';
this.domManager.dropdown.removeAttribute('data-visible');
this.isDropdownVisible = false;
if (this.debugMode) ;
}
/**
* Posiciona o dropdown corretamente - Versão Robusta
*/
positionDropdown() {
if (!this.domManager.countryContainer) {
console.error('countryContainer não encontrado para posicionamento');
return;
}
const buttonRect = this.domManager.countryContainer.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
if (this.debugMode) ;
// Posição base abaixo do botão
let top = buttonRect.bottom + 5; // Espaço maior
let left = buttonRect.left;
// Largura do dropdown
const dropdownWidth = Math.max(buttonRect.width, 200); // Largura mínima maior
// Verificar se há espaço suficiente abaixo
const spaceBelow = viewportHeight - buttonRect.bottom;
const dropdownHeight = Math.min(250, this.domManager.dropdown.scrollHeight || 200);
if (this.debugMode) ;
// Se não há espaço suficiente abaixo, mostrar acima
if (spaceBelow < dropdownHeight && buttonRect.top > dropdownHeight) {
top = buttonRect.top - dropdownHeight - 5;
if (this.debugMode) ;
}
// Garantir que não saia da tela horizontalmente
if (left + dropdownWidth > viewportWidth) {
left = viewportWidth - dropdownWidth - 20;
if (this.debugMode) ;
}
if (left < 20) {
left = 20;
}
// Garantir que não saia da tela verticalmente
if (top < 20) {
top = 20;
}
if (top + dropdownHeight > viewportHeight) {
top = viewportHeight - dropdownHeight - 20;
}
if (this.debugMode) ;
// Aplicar posicionamento
Object.assign(this.domManager.dropdown.style, {
top: `${top}px`,
left: `${left}px`,
width: `${dropdownWidth}px`,
maxHeight: `${dropdownHeight}px`
});
}
/**
* Verifica se o elemento está na viewport
*/
isInViewport(rect) {
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
/**
* Debug completo do estado atual
*/
debugCurrentState() {
debug('Estado interno:', {
selectedCountry: this.selectedCountry,
isDropdownVisible: this.isDropdownVisible,
domManagerExists: !!this.domManager,
dropdownExists: !!this.domManager?.dropdown
});
if (this.domManager?.dropdown) {
this.domManager.debugDropdown();
}
}
/**
* Executa GeoIP lookup se configurado
*/
autoGeolocate(options) {
if (!options.autoGeolocate) return;
if (options.geoIpLookup && typeof options.geoIpLookup === 'function') {
options.geoIpLookup((countryCode) => {
if (countryCode && this.countries[countryCode.toLowerCase()]) {
this.selectCountry(countryCode.toLowerCase());
}
});
return;
}
this.defaultGeoIpLookup((countryCode) => {
if (countryCode && this.countries[countryCode.toLowerCase()]) {
this.selectCountry(countryCode.toLowerCase());
}
});
}
/**
* GeoIP lookup padrão
*/
defaultGeoIpLookup(callback) {
fetch("https://ipapi.co/json")
.then(res => res.json())
.then(data => {
debug('GeoIP detected country:', data.country_code);
callback(data.country_code);
})
.catch((error) => {
callback('br');
});
}
/**
* Retorna o país selecionado
*/
getSelectedCountry() {
return this.selectedCountry;
}
/**
* Retorna dados do país selecionado
*/
getSelectedCountryData() {
return this.countries[this.selectedCountry];
}
/**
* Ativa/desativa modo debug
*/
setDebugMode(enabled) {
this.debugMode = enabled;
}
/**
* Callback para mudança de país (deve ser definido externamente)
*/
onCountryChange = null;
}
/**
* DOMManager - Versão Robusta Anti-Conflito
* Gerencia criação e manipulação de elementos DOM
*/
class DOMManager {
constructor(input, countries) {
this.input = input;
this.countries = countries;
this.wrapper = null;
this.countryContainer = null;
this.countryButton = null;
this.dropdown = null;
this.uniqueId = 'tax-dropdown-' + Math.random().toString(36).substr(2, 9);
}
/**
* Cria a estrutura DOM do plugin
*/
createWrapper() {
this.wrapper = document.createElement('div');
this.wrapper.className = 'tax-document-input';
this.input.parentNode.insertBefore(this.wrapper, this.input);
this.wrapper.appendChild(this.input);
this.input.className += ' tax-document-input__field';
this.addStyles();
}
/**
* Cria o seletor de país com dropdown
*/
createCountrySelector() {
this.countryContainer = document.createElement('div');
this.countryContainer.className = 'tax-document-input__country';
this.countryButton = document.createElement('button');
this.countryButton.type = 'button';
this.countryButton.className = 'tax-document-input__country-button';
// Criar o dropdown com ID único para evitar conflitos
this.dropdown = document.createElement('ul');
this.dropdown.className = 'tax-document-input__dropdown';
this.dropdown.id = this.uniqueId;
this.dropdown.setAttribute('data-tax-dropdown', 'true');
// ESTILOS INLINE CRÍTICOS para evitar conflitos de CSS
this.applyInlineStyles();
this.countryContainer.appendChild(this.countryButton);
document.body.appendChild(this.dropdown);
this.wrapper.insertBefore(this.countryContainer, this.input);
debug('Dropdown criado com ID único:', this.uniqueId);
}
/**
* Aplica estilos inline críticos que não podem ser sobrescritos
*/
applyInlineStyles() {
const criticalStyles = {
position: 'fixed',
zIndex: '2147483647', // Máximo z-index possível
margin: '0',
padding: '0',
listStyle: 'none',
display: 'none',
backgroundColor: 'white',
border: '1px solid #ccc',
borderRadius: '4px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
maxHeight: '200px',
overflowY: 'auto',
minWidth: '150px',
opacity: '1',
visibility: 'visible',
pointerEvents: 'auto',
fontSize: '14px',
fontFamily: 'Arial, sans-serif',
boxSizing: 'border-box'
};
Object.assign(this.dropdown.style, criticalStyles);
}
/**
* Força a exibição do dropdown com estilos inline
*/
forceShowDropdown() {
const forceStyles = {
display: 'block !important',
position: 'fixed !important',
zIndex: '2147483647 !important',
backgroundColor: 'white !important',
border: '2px solid #007bff !important',
visibility: 'visible !important',
opacity: '1 !important',
pointerEvents: 'auto !important'
};
// Aplicar via cssText para garantir !important
let cssText = '';
Object.entries(forceStyles).forEach(([prop, value]) => {
const cssProp = prop.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
cssText += `${cssProp}: ${value}; `;
});
this.dropdown.style.cssText += cssText;
}
/**
* Popula o dropdown com países disponíveis
*/
populateCountries(availableCountries, onCountrySelect) {
this.dropdown.innerHTML = '';
availableCountries.forEach(countryCode => {
if (this.countries[countryCode]) {
const country = this.countries[countryCode];
debug('Criando item para país:', country.name);
const li = document.createElement('li');
li.className = 'tax-document-input__dropdown-item';
li.setAttribute('data-country', countryCode);
// Estilos inline para os itens também
Object.assign(li.style, {
padding: '8px 12px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
backgroundColor: 'white',
borderBottom: '1px solid #eee',
fontSize: '14px',
color: '#333',
boxSizing: 'border-box'
});
li.innerHTML = `
<img src="${country.flag}" alt="${country.name}"
style="width: 20px; height: 15px; object-fit: cover; border-radius: 2px;" />
<span style="font-size: 14px; color: #333;">${country.name}</span>
`;
// Event listener com prevenção de propagação
li.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (onCountrySelect) {
onCountrySelect(countryCode);
}
});
// Hover effect inline
li.addEventListener('mouseenter', () => {
li.style.backgroundColor = '#f5f5f5';
});
li.addEventListener('mouseleave', () => {
li.style.backgroundColor = 'white';
});
this.dropdown.appendChild(li);
}
});
debug('Total de itens criados no dropdown:', this.dropdown.children.length);
}
/**
* Atualiza a bandeira exibida no botão
*/
updateSelectedCountry(country) {
if (country) {
this.countryButton.innerHTML = `
<img src="${country.flag}" alt="${country.name}"
style="width: 20px; height: 15px; object-fit: cover; border-radius: 2px;" />
<span style="font-size: 10px; color: #666;">▼</span>
`;
}
}
/**
* Adiciona classe de estado de validação no input
*/
setValidationState(isValid) {
this.input.classList.remove('tax-document-input--valid', 'tax-document-input--invalid');
if (isValid === true) {
this.input.classList.add('tax-document-input--valid');
} else if (isValid === false) {
this