operator-cursor
Version:
👻 A phantom cursor that navigates and interacts with web pages like a human operator
407 lines (362 loc) • 13.7 kB
JavaScript
// ==========================================
// 📄 src/effects/effect-manager.js
// ==========================================
import { VISUAL_EFFECTS_CONFIG, EFFECT_COLORS } from './visual-effects.js';
/**
* 🤖 Gestor principal de efectos visuales para OperatorCursor
* Maneja la aplicación de efectos, creación de eventos y gestión de CSS
*/
export class EffectManager {
/**
* Aplicar efecto visual a un elemento
* @param {HTMLElement} element - Elemento DOM
* @param {string} eventType - Tipo de evento (click, hover, etc.)
* @returns {boolean} - true si se aplicó el efecto, false si no
*/
static applyEffect(element, eventType) {
const effect = VISUAL_EFFECTS_CONFIG[eventType];
if (!effect) {
console.warn(`[OperatorCursor] No effect configured for event type: ${eventType}`);
return false;
}
const { class: className, duration, remove } = effect;
try {
if (remove) {
// Remover la clase inmediatamente
element.classList.remove(className);
console.debug(`[OperatorCursor] Removed class '${className}' from element`, element);
} else {
// Añadir la clase
element.classList.add(className);
console.debug(`[OperatorCursor] Applied class '${className}' to element`, element);
// Si tiene duración, remover después del tiempo especificado
if (duration > 0) {
setTimeout(() => {
element.classList.remove(className);
console.debug(`[OperatorCursor] Auto-removed class '${className}' after ${duration}ms`, element);
}, duration);
}
}
return true;
} catch (error) {
console.error(`[OperatorCursor] Error applying effect:`, error);
return false;
}
}
/**
* Crear evento especÃfico según el tipo
* @param {string} type - Tipo de evento
* @param {HTMLElement} element - Elemento objetivo
* @returns {Event} - Evento creado
*/
static createEventByType(type, element) {
const commonProps = { bubbles: true, cancelable: true };
try {
switch(type) {
case 'click':
case 'dblclick':
case 'mousedown':
case 'mouseup':
case 'mouseenter':
case 'mouseleave':
return new MouseEvent(type, {
...commonProps,
view: window,
button: 0,
clientX: element.offsetLeft + element.offsetWidth / 2,
clientY: element.offsetTop + element.offsetHeight / 2,
screenX: element.offsetLeft + element.offsetWidth / 2,
screenY: element.offsetTop + element.offsetHeight / 2
});
case 'keydown':
case 'keyup':
return new KeyboardEvent(type, {
...commonProps,
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13
});
case 'input':
case 'change':
return new Event(type, { ...commonProps });
case 'focus':
case 'blur':
return new FocusEvent(type, {
...commonProps,
relatedTarget: null
});
case 'submit':
return new Event(type, { ...commonProps });
case 'scroll':
return new Event(type, { ...commonProps });
default:
console.warn(`[OperatorCursor] Unknown event type '${type}', creating generic Event`);
return new Event(type, commonProps);
}
} catch (error) {
console.error(`[OperatorCursor] Error creating event of type '${type}':`, error);
return new Event(type, commonProps);
}
}
/**
* Obtener información del elemento para logging
* @param {HTMLElement} element - Elemento DOM
* @returns {Object} - Información del elemento
*/
static getElementInfo(element) {
try {
return {
tagName: element.tagName,
id: element.id || null,
className: element.className || null,
textContent: element.textContent?.substring(0, 50) || null,
attributes: Array.from(element.attributes).reduce((acc, attr) => {
acc[attr.name] = attr.value;
return acc;
}, {}),
position: {
offsetTop: element.offsetTop,
offsetLeft: element.offsetLeft,
offsetWidth: element.offsetWidth,
offsetHeight: element.offsetHeight
}
};
} catch (error) {
console.error(`[OperatorCursor] Error getting element info:`, error);
return {
tagName: element.tagName || 'unknown',
error: error.message
};
}
}
/**
* Inyectar CSS de efectos visuales en el documento
*/
static injectCSS() {
// Evitar inyección duplicada - 🤖 CAMBIO: nuevo ID
if (document.getElementById('operator-cursor-visual-effects-css')) {
console.debug('[OperatorCursor] CSS already injected');
return;
}
try {
const style = document.createElement('style');
style.id = 'operator-cursor-visual-effects-css'; // 🤖 CAMBIO: nuevo ID
style.textContent = this.generateCSS();
document.head.appendChild(style);
console.log('[OperatorCursor] CSS injected successfully');
} catch (error) {
console.error('[OperatorCursor] Error injecting CSS:', error);
}
}
/**
* Generar CSS dinámico para los efectos
* @returns {string} - CSS completo
*/
static generateCSS() {
return `
/* 🤖 OperatorCursor - Visual Effects System */
/* Generated automatically - Do not edit manually */
/* Active effects (click, dblclick) */
.operator-effect-active {
transform: scale(0.95) !important;
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 0 15px ${EFFECT_COLORS.primary} !important;
filter: brightness(1.1) !important;
}
/* Pressed effect (mousedown) */
.operator-effect-pressed {
transform: scale(0.98) !important;
transition: transform 0.1s ease !important;
opacity: 0.8 !important;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2) !important;
}
/* Released effect (mouseup) */
.operator-effect-released {
transform: scale(1.02) !important;
transition: transform 0.05s ease-out !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
}
/* Hover effect (mouseenter/mouseleave) */
.operator-effect-hover {
background-color: ${EFFECT_COLORS.primary} !important;
transition: background-color 0.2s ease !important;
cursor: pointer !important;
border-radius: 4px !important;
}
/* Focus effect */
.operator-effect-focused {
outline: 2px solid ${EFFECT_COLORS.focus} !important;
outline-offset: 2px !important;
box-shadow: 0 0 0 4px rgba(0, 93, 255, 0.2) !important;
transition: box-shadow 0.2s ease !important;
}
/* Submit effect */
.operator-effect-submitting {
background-color: ${EFFECT_COLORS.success} !important;
border: 2px solid rgba(40, 167, 69, 0.8) !important;
transform: scale(1.01) !important;
transition: all 0.3s ease !important;
animation: operator-pulse-submit 0.3s ease !important;
}
/* Input change effect */
.operator-effect-changed {
border-color: ${EFFECT_COLORS.success.replace('0.3', '0.8')} !important;
box-shadow: 0 0 8px ${EFFECT_COLORS.success} !important;
transition: all 0.2s ease !important;
}
/* Typing effect */
.operator-effect-typing {
border-color: ${EFFECT_COLORS.info.replace('0.3', '0.6')} !important;
box-shadow: 0 0 5px ${EFFECT_COLORS.info} !important;
transition: all 0.1s ease !important;
}
/* Key press effects */
.operator-effect-keypress {
background-color: ${EFFECT_COLORS.warning} !important;
transform: scale(0.99) !important;
transition: all 0.08s ease !important;
}
.operator-effect-keyrelease {
background-color: ${EFFECT_COLORS.info} !important;
transform: scale(1.01) !important;
transition: all 0.04s ease !important;
}
/* Scroll effect */
.operator-effect-scrolling {
box-shadow: inset 0 0 10px ${EFFECT_COLORS.primary} !important;
transition: box-shadow 0.1s ease !important;
border-left: 3px solid ${EFFECT_COLORS.focus} !important;
}
/* 🤖 Animaciones operador personalizadas */
@keyframes operator-pulse-submit {
0% { transform: scale(1.01); }
50% { transform: scale(1.05); }
100% { transform: scale(1.01); }
}
@keyframes operator-glow {
0%, 100% { box-shadow: 0 0 5px rgba(0, 120, 215, 0.3); }
50% { box-shadow: 0 0 20px rgba(0, 120, 215, 0.8); }
}
/* Efectos para diferentes tipos de elementos */
button.operator-effect-active,
input[type="button"].operator-effect-active,
input[type="submit"].operator-effect-active {
transform: scale(0.96) !important;
}
input.operator-effect-focused,
textarea.operator-effect-focused,
select.operator-effect-focused {
border-width: 2px !important;
}
/* Efectos responsivos */
@media (max-width: 768px) {
.operator-effect-active {
transform: scale(0.97) !important;
}
.operator-effect-pressed {
transform: scale(0.99) !important;
}
}
/* Modo de alto contraste */
@media (prefers-contrast: high) {
.operator-effect-focused {
outline-width: 3px !important;
outline-color: #000 !important;
}
}
/* Reducir animaciones si el usuario lo prefiere */
@media (prefers-reduced-motion: reduce) {
.operator-effect-active,
.operator-effect-pressed,
.operator-effect-released,
.operator-effect-hover,
.operator-effect-focused,
.operator-effect-submitting,
.operator-effect-changed,
.operator-effect-typing,
.operator-effect-keypress,
.operator-effect-keyrelease,
.operator-effect-scrolling {
transition: none !important;
animation: none !important;
}
}
`;
}
/**
* Remover CSS de efectos del documento
*/
static removeCSS() {
const style = document.getElementById('operator-cursor-visual-effects-css'); // 🤖 CAMBIO: nuevo ID
if (style) {
style.remove();
console.log('[OperatorCursor] CSS removed');
}
}
/**
* Verificar si los efectos están disponibles y funcionando
* @returns {Object} - Estado del sistema de efectos
*/
static getStatus() {
return {
cssInjected: !!document.getElementById('operator-cursor-visual-effects-css'), // 🤖 CAMBIO: nuevo ID
configuredEvents: Object.keys(VISUAL_EFFECTS_CONFIG),
supportedEvents: EVENT_TYPES,
version: '1.0.0',
ready: true
};
}
/**
* Limpiar todos los efectos activos de un elemento
* @param {HTMLElement} element - Elemento a limpiar
*/
static clearAllEffects(element) {
const allEffectClasses = Object.values(VISUAL_EFFECTS_CONFIG).map(effect => effect.class);
element.classList.remove(...allEffectClasses);
console.debug('[OperatorCursor] Cleared all effects from element', element);
}
/**
* Configurar nuevo efecto personalizado
* @param {string} eventType - Tipo de evento
* @param {Object} config - Configuración del efecto
*/
static addCustomEffect(eventType, config) {
VISUAL_EFFECTS_CONFIG[eventType] = {
class: config.class || `operator-effect-${eventType}`, // 🤖 CAMBIO: nuevo prefijo
duration: config.duration || 150,
description: config.description || `Custom effect for ${eventType}`,
...config
};
console.log(`[OperatorCursor] Added custom effect for '${eventType}'`);
}
/**
* 🤖 Registrar OperatorCursor en el sistema global
*/
static registerOperatorCursor(OperatorCursorClass) {
if (typeof window !== 'undefined') {
window.OperatorCursor = window.OperatorCursor || {}; // 🤖 CAMBIO: nuevo namespace
window.OperatorCursor.Cursor = OperatorCursorClass;
window.OperatorCursor.EffectManager = EffectManager;
console.log('[OperatorCursor] OperatorCursor registered globally');
}
}
/**
* 🤖 Obtener instancia singleton del cursor operador
*/
static getOperatorCursor() {
if (typeof window !== 'undefined' && window.OperatorCursor && window.OperatorCursor._operatorInstance) {
return window.OperatorCursor._operatorInstance; // 🤖 CAMBIO: nuevo nombre
}
return null;
}
/**
* 🤖 Limpiar cursor operador
*/
static cleanupOperatorCursor() {
if (typeof window !== 'undefined' && window.OperatorCursor && window.OperatorCursor._operatorInstance) {
window.OperatorCursor._operatorInstance.destroy();
delete window.OperatorCursor._operatorInstance; // 🤖 CAMBIO: nuevo nombre
}
}
}