O SmartEdgeFilter é um motor de exibição/ocultação de elementos HTML desenvolvido em JavaScript. Ele permite controlar a visibilidade de qualquer componente da interface (DIVs, SPANs, botões, etc.) com base nas informações dinâmicas fornecidas por nossos endpoints JSON (e.g., carrinho, usuário, cotação).
Esta solução elimina a necessidade de escrever lógica condicional complexa em templates (como Twig ou Blade), movendo a regra de apresentação para o próprio HTML, resultando em:
HTML mais limpo: A lógica é declarativa via atributos
data-edge-*.Maior Coerência: A apresentação reflete fielmente o estado do backend.
Melhor Desempenho: Utiliza
MutationObserverpara processamento eficiente de elementos carregados dinamicamente.
Como Usar
Para ativar a funcionalidade, basta adicionar o atributo data-edge-filter a qualquer tag HTML e configurá-lo com os atributos de controle abaixo.
1. Atributos de Controle
Atributo | Obrigatório | Descrição | Exemplo de Valor |
|---|---|---|---|
| Sim | Ativa o motor de filtragem neste elemento. |
|
| Sim | Nome do endpoint onde os dados residem. |
|
| Sim | Caminho da propriedade dentro do JSON. Suporta dot notation para aninhamento. |
|
| Não | Regra de avaliação (padrão é |
|
| Não | Valor de referência para comparação (usado com |
|
2. Condições de Avaliação Disponíveis
Condição | Descrição | Exemplo Prático |
|---|---|---|
| O valor existe (não é nulo/vazio). Para arrays/strings, verifica se |
|
| O valor está ausente ou vazio. Para arrays/strings, verifica se |
|
| O valor é estritamente igual ao |
|
| O valor é estritamente diferente do |
|
| O valor numérico é maior que o |
|
| O valor numérico é menor que o |
|
| O valor (string ou array) contém o |
|
| O valor está presente em uma lista de valores separados por vírgula. |
|
Exemplos de Uso (Copie e Cole)
Cenário 1: Usuário Logado e Dados Aninhados
Ação | Código HTML |
|---|---|
Exibir "Bem-vindo" se |
|
Exibir se o primeiro cliente de um agente tiver um |
|
Cenário 2: Verificando Quantidade e Vazios
Ação | Código HTML |
|---|---|
Exibir o ícone do carrinho se |
|
Exibir mensagem se a lista de |
|
Exibir se a cotação estiver zerada ( |
|
Cenário 3: Validação de Valores em Lista (inArray)
Ação | Código HTML |
|---|---|
Exibir botão especial se o nome do agente estiver em uma lista específica. |
|
Exibir aviso se o status do pedido estiver em estados críticos. |
|
Exibir badge para categorias permitidas. |
|
Detalhes Técnicos e Design
1. Endpoints Mapeados
O motor utiliza o objeto ENDPOINT_DATA para buscar os valores. Os endpoints atualmente mapeados são:
user-summaryquote-summarycart-summaryagent-customer-selector
2. Padrões de Código
O SmartEdgeFilterEngine segue rigorosamente os padrões de Clean Code:
Single Responsibility Principle (SRP):
PathResolver: Apenas resolve caminhos aninhados.ConditionEvaluator: Apenas executa a lógica de comparação.applyFilter: Apenas orquestra e aplica a visibilidade.
Compatibilidade e Desempenho:
Funciona com qualquer tag HTML.
Utiliza o
MutationObserverpara monitorar elementos injetados dinamicamente na página (por AJAX ou frameworks), garantindo que os filtros sejam aplicados em tempo real sem polling (verificação constante).
Fallback de Segurança: Se um endpoint ou path não for encontrado, o elemento é automaticamente ocultado (
display: none), priorizando a segurança e evitando a exibição de conteúdo incorreto.
Sistema de Debug
Ativar Debug
Abra o Console do Navegador (F12) e execute:
localStorage.setItem('FLEXY_DEBUG', 'true'); location.reload();Desativar Debug
localStorage.removeItem('FLEXY_DEBUG'); location.reload();Código
Estou adicionando esse código pelo gerenciador de arquivos, com o nome smart-edge-filter.js
Adicione imediatamente abaixo do widgets.js.
// Aguarda o evento ENDPOINT_DATA_READY ser disparado window.addEventListener("ENDPOINT_DATA_READY", () => { //console.log('✅ Dados dos endpoints carregados, iniciando SmartEdge...'); const script = document.createElement("script"); script.src = "{URL}/smart-edge-filter.js"; document.body.appendChild(script); });O código na íntegra:
/**
* SmartEdgeFilterEngine v2.0
* Motor de exibição/ocultação de elementos baseado em dados de endpoints.
* Utiliza o padrão Módulo para Clean Code, encapsulamento e Single Responsibility.
*
* NOVO v2.0: Suporte a múltiplas condicionais (AND/OR) sem quebrar compatibilidade
*
* IMPORTANTE: Este script só deve ser executado APÓS o evento ENDPOINT_DATA_READY
* ser disparado pelo widgets-v2.js, garantindo que todos os dados estejam carregados.
*
* Marcos Lunardelli <marcos.lunardelli@flexy.com.br>
*/
const SmartEdgeFilterEngine = (() => {
// ---------------------------------------------------------
// DEBUG (compatível com Widgets, não alterar nome)
// ---------------------------------------------------------
const DEBUG_MODE = localStorage.getItem('FLEXY_DEBUG') === 'true';
const sedebug = (...args) => DEBUG_MODE && console.log('[SMART-EDGE]', ...args);
const sedebugError = (...args) => DEBUG_MODE && console.error('[SMART-EDGE]', ...args);
/** Verifica se um valor é null ou undefined. */
const isNullOrUndefined = (v) => v === null || typeof v === 'undefined';
// ----------------------------------------------------------------------
// # PathResolver - Obtém valor aninhado no JSON (dot notation)
// ----------------------------------------------------------------------
const PathResolver = (obj, path) => {
if (!obj || !path) return undefined;
const pathParts = path.split('.')
.map(part => {
const match = part.match(/(\w+)\[(\d+)\]/);
return match ? [match[1], parseInt(match[2])] : [part];
})
.flat()
.filter(p => p !== '');
let current = obj;
for (const part of pathParts) {
if (isNullOrUndefined(current)) return undefined;
current = current[part];
}
return current;
};
// ----------------------------------------------------------------------
// # ConditionEvaluator - Avalia condições de filtragem
// ----------------------------------------------------------------------
const ConditionEvaluator = (value, condition, referenceValue) => {
let ref = referenceValue;
let val = value;
if (typeof val === 'number' && !isNaN(parseFloat(ref))) {
ref = parseFloat(ref);
} else if (typeof val === 'boolean' || ref === 'true' || ref === 'false') {
ref = ref === 'true';
} else if (ref === 'null') {
ref = null;
}
const hasLength = (v) => Array.isArray(v) || typeof v === 'string';
switch (condition) {
case 'hasValue':
if (hasLength(val)) return val.length > 0;
if (typeof val === 'boolean') return val === true;
return !isNullOrUndefined(val);
case 'isEmpty':
if (hasLength(val)) return val.length === 0;
return isNullOrUndefined(val);
case 'equals':
return val === ref;
case 'notEquals':
return val !== ref;
case 'greaterThan':
return typeof val === 'number' && val > ref;
case 'lessThan':
return typeof val === 'number' && val < ref;
case 'contains':
if (typeof val === 'string') return val.includes(String(ref));
if (Array.isArray(val)) return val.some(item => String(item).includes(String(ref)));
return false;
case 'inArray':
const allowedValues = referenceValue.split(',').map(v => v.trim());
return allowedValues.includes(String(val));
default:
return false;
}
};
// ----------------------------------------------------------------------
// # CollectConditions - Coleta todas as condições de um elemento (v2.0)
// ----------------------------------------------------------------------
const CollectConditions = (element) => {
const conditions = [];
// Primeira condição (sem sufixo) - compatibilidade v1
if (element.dataset.edgeEndpoint && element.dataset.edgePath) {
conditions.push({
endpoint: element.dataset.edgeEndpoint,
path: element.dataset.edgePath,
condition: element.dataset.edgeCondition || 'hasValue',
value: element.dataset.edgeValue
});
}
// Condições adicionais (sufixo -2, -3, -4, etc.)
let index = 2;
while (element.dataset[`edgeEndpoint${index}`] && element.dataset[`edgePath${index}`]) {
conditions.push({
endpoint: element.dataset[`edgeEndpoint${index}`],
path: element.dataset[`edgePath${index}`],
condition: element.dataset[`edgeCondition${index}`] || 'hasValue',
value: element.dataset[`edgeValue${index}`]
});
index++;
}
return conditions;
};
// ----------------------------------------------------------------------
// # EvaluateMultipleConditions - Avalia múltiplas condições com operador lógico (v2.0)
// ----------------------------------------------------------------------
const EvaluateMultipleConditions = (conditions, operator) => {
if (conditions.length === 0) return false;
if (conditions.length === 1) {
// Compatibilidade v1 - apenas uma condição
return conditions[0].result;
}
// v2.0 - Múltiplas condições
if (operator === 'OR') {
return conditions.some(c => c.result === true);
}
// AND (padrão)
return conditions.every(c => c.result === true);
};
// ----------------------------------------------------------------------
// # Fallback seguro de display baseado na tag
// ----------------------------------------------------------------------
const getSafeFallbackDisplay = (tag) => {
tag = tag.toLowerCase();
if (["a", "span", "strong", "em", "i", "b", "small"].includes(tag)) {
return "inline";
}
if (["img", "input", "button", "select", "label"].includes(tag)) {
return "inline-block";
}
return "block"; // default para elementos estruturais
};
// ----------------------------------------------------------------------
// # getRealDisplay - ignora estilos inline e captura o display "real"
// ----------------------------------------------------------------------
const getRealDisplay = (element) => {
try {
const clone = element.cloneNode(true);
// Remove atributos data-edge-* para evitar interferência
const edgeAttrs = Array.from(clone.attributes)
.filter(attr => attr.name.startsWith('data-edge-'));
edgeAttrs.forEach(attr => clone.removeAttribute(attr.name));
// Remove qualquer estilo inline
clone.removeAttribute("style");
// Garante que ele não aparece e não polui layout
clone.style.position = "absolute";
clone.style.visibility = "hidden";
clone.style.pointerEvents = "none";
clone.style.top = "-9999px";
// Usa um container neutro para evitar herança de CSS
let container = document.getElementById("edge-temp-measure");
if (!container) {
container = document.createElement("div");
container.id = "edge-temp-measure";
container.style.all = "unset";
container.style.position = "absolute";
container.style.visibility = "hidden";
container.style.pointerEvents = "none";
container.style.top = "-9999px";
document.body.appendChild(container);
}
container.appendChild(clone);
const display = window.getComputedStyle(clone).display;
clone.remove();
return display && display !== "none" ? display : "";
} catch (e) {
return "";
}
};
// ----------------------------------------------------------------------
// # Aplica o filtro a um elemento individual (v2.0 - Multi-conditional)
// ----------------------------------------------------------------------
const applyFilter = (element) => {
try {
sedebug('➡️ Processando elemento:', element);
// Coleta todas as condições
const conditions = CollectConditions(element);
if (conditions.length === 0) {
sedebug('❌ Nenhuma condição definida. Ocultando por segurança.');
element.style.setProperty('display', 'none', 'important');
return;
}
sedebug(`📋 Total de condições encontradas: ${conditions.length}`);
// Salva display original apenas na primeira execução
if (!element.dataset.originalDisplay) {
const realDisplay = getRealDisplay(element);
element.dataset.originalDisplay = realDisplay;
sedebug('💾 Display ORIGINAL (real, ignorando inline):', realDisplay);
}
// Avalia cada condição individualmente
const evaluatedConditions = conditions.map((cond, idx) => {
sedebug(`📄 Condição ${idx + 1}: endpoint=${cond.endpoint}, path=${cond.path}, condition=${cond.condition}, value=${cond.value}`);
if (!window.ENDPOINT_DATA || !window.ENDPOINT_DATA[cond.endpoint]) {
sedebug(`❌ Endpoint '${cond.endpoint}' não encontrado em ENDPOINT_DATA`);
return { ...cond, result: false };
}
const endpointData = window.ENDPOINT_DATA[cond.endpoint];
const resolvedValue = PathResolver(endpointData, cond.path);
sedebug(`🔎 Valor resolvido (condição ${idx + 1}):`, resolvedValue);
const isValueResolved = !isNullOrUndefined(resolvedValue);
const result = isValueResolved
? ConditionEvaluator(resolvedValue, cond.condition, cond.value)
: cond.condition === 'isEmpty';
sedebug(`📌 Resultado condição ${idx + 1}: ${result ? 'TRUE' : 'FALSE'}`);
return { ...cond, result };
});
// Obtém operador lógico (AND por padrão, compatível com v1)
const operator = (element.dataset.edgeOperator || 'AND').toUpperCase();
sedebug(`🔗 Operador lógico: ${operator}`);
// Avalia resultado final
const shouldShow = EvaluateMultipleConditions(evaluatedConditions, operator);
sedebug(`🎯 Resultado FINAL: ${shouldShow ? 'MOSTRAR' : 'OCULTAR'}`);
if (shouldShow) {
let finalDisplay = element.dataset.originalDisplay;
if (!finalDisplay || finalDisplay.trim() === "") {
finalDisplay = getSafeFallbackDisplay(element.tagName);
}
sedebug('📤 Aplicando display:', finalDisplay);
element.style.setProperty('display', finalDisplay, 'important');
} else {
sedebug('📤 Aplicando display: none !important');
element.style.setProperty('display', 'none', 'important');
}
} catch (err) {
sedebugError('💥 Erro ao aplicar filtro:', err, element);
element.style.setProperty('display', 'none', 'important');
}
};
/** Varre todos os elementos com data-edge-filter */
const processElements = (container = document) => {
sedebug('🔄 Varredura em container:', container);
const elementsToFilter = container.querySelectorAll('[data-edge-filter]');
sedebug(`📊 Total de elementos encontrados: ${elementsToFilter.length}`);
elementsToFilter.forEach(applyFilter);
sedebug('✅ Varredura concluída');
};
// ----------------------------------------------------------------------
// # Inicializador - Estratégia Híbrida (100% assertivo)
// ----------------------------------------------------------------------
const Initializer = () => {
sedebug('🚀 SmartEdgeFilterEngine v2.0 iniciando...');
const processData = () => {
sedebug('🏁 Processando elementos...');
sedebug('📋 Endpoints disponíveis:', Object.keys(window.ENDPOINT_DATA));
sedebug('📄 Dados completos:', window.ENDPOINT_DATA);
processElements(document);
sedebug('👀 Ativando MutationObserver...');
const observer = new MutationObserver(mutationsList => {
mutationsList.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (node.hasAttribute && node.hasAttribute('data-edge-filter')) {
applyFilter(node);
}
if (node.querySelectorAll) {
const children = node.querySelectorAll('[data-edge-filter]');
if (children.length > 0) {
children.forEach(applyFilter);
}
}
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
sedebug('✅ SmartEdgeFilterEngine v2.0 totalmente inicializado!');
};
const hasRequiredData = () => {
if (!window.ENDPOINT_DATA) {
sedebug('⚠️ window.ENDPOINT_DATA não existe');
return false;
}
const required = ['user-summary', 'cart-summary', 'quote-summary'];
const allPresent = required.every(key => {
const data = window.ENDPOINT_DATA[key];
const hasData = data && typeof data === 'object' && Object.keys(data).length > 0;
if (!hasData) {
sedebug(`⚠️ Endpoint '${key}' não tem dados`);
}
return hasData;
});
return allPresent;
};
if (hasRequiredData()) {
sedebug('⚡ Dados já disponíveis! Processando imediatamente...');
setTimeout(processData, 50);
} else {
sedebug('⏳ Dados ainda não disponíveis, registrando listener...');
window.addEventListener('ENDPOINT_DATA_READY', () => {
sedebug('🎯 Evento ENDPOINT_DATA_READY recebido!');
setTimeout(processData, 50);
}, { once: true });
}
};
return {
init: Initializer,
refresh: () => {
sedebug('🔄 Refresh manual solicitado');
processElements(document);
},
version: '2.0.0'
};
})();
try {
SmartEdgeFilterEngine.init();
} catch (e) {
console.error('[SMART-EDGE] ❌ Erro ao inicializar:', e);
}
window.SmartEdgeFilterEngine = SmartEdgeFilterEngine;
Comentários
0 comentário
Por favor, entre para comentar.