/**
 * @archivo eventos.js. Un sistema de eventos (John Resig - Secretos de un JS Ninja http://jsninja.com/)
 * (La versión del libro original no se podía usar por completo, por lo que se arreglaron algunas cosas y se hizo compatible con Closure Compiler)
 * Esto debería funcionar de manera muy similar a los eventos de jQuery, sin embargo, se basa en la versión del libro, que no es tan
 * robusto como el de jquery, por lo que probablemente haya algunas diferencias.
 *
 * @archivo eventos.js
 * @module eventos
 * /
importar DomData desde './dom-data';
importar * como Guid desde './guid.js';
importar registro desde './log.js';
importar ventana desde 'global/window';
importar documento desde 'global/document';

/ **
 * Limpiar el caché del oyente y los despachadores
 *
 * @param {Elemento|Objeto} elemento
 * Elemento para limpiar
 *
 * @param {cadena} tipo
 * Tipo de evento a limpiar
 * /
función _cleanUpEvents (elemento, tipo) {
  si (!DomData.has(elem)) {
    devolver;
  }
  datos constantes = DomData.get(elem);

  // Elimina los eventos de un tipo en particular si no queda ninguno
  if (data.handlers[tipo].longitud === 0) {
    eliminar data.handlers[tipo];
    // data.handlers[tipo] = null;
    // Establecer en nulo estaba causando un error con data.handlers

    // Eliminar el metamanejador del elemento
    si (elem.removeEventListener) {
      elem.removeEventListener(type, data.dispatcher, false);
    } más si (elem.detachEvent) {
      elem.detachEvent('on' + tipo, data.dispatcher);
    }
  }

  // Eliminar el objeto de eventos si no quedan tipos
  if (Object.getOwnPropertyNames(data.handlers).length < = 0) {
    eliminar manejadores de datos;
    eliminar data.dispatcher;
    eliminar datos.deshabilitado;
  }

  // Finalmente, elimine los datos del elemento si no quedan datos
  if (Objeto.getOwnPropertyNames(datos).longitud === 0) {
    DomData.delete(elemento);
  }
}

/ **
 * Recorre una serie de tipos de eventos y llama al método solicitado para cada tipo.
 *
 * @param {Función} fn
 * El método de evento que queremos usar.
 *
 * @param {Elemento|Objeto} elemento
 * Elemento u objeto para vincular a los oyentes
 *
 * @param {cadena} tipo
 * Tipo de evento al que vincularse.
 *
 * @param {EventTarget~EventListener} devolución de llamada
 * Oyente de eventos.
 * /
function _handleMultipleEvents(fn, elem, tipos, devolución de llamada) {
  tipos.forEach(función(tipo) {
    // Llamar al método de evento para cada uno de los tipos
    fn(elemento, tipo, devolución de llamada);
  });
}

/ **
 * Arreglar un evento nativo para tener valores de propiedad estándar
 *
 * evento @param {Objeto}
 * Objeto de evento para arreglar.
 *
 * @return {Objeto}
 * Objeto de evento fijo.
 * /
función de exportación fixEvent (evento) {
  si (evento.fijo_) {
    evento de retorno;
  }

  función devuelveVerdadero() {
    devolver verdadero;
  }

  función devolverFalso() {
    falso retorno;
  }

  // Probar si es necesario arreglar
  // Se usa para verificar si !event.stopPropagation en lugar de isPropagationStopped
  // Pero los eventos nativos devuelven verdadero para stopPropagation, pero no tienen
  // otros métodos esperados como isPropagationStopped. parece ser un problema
  // con el código Javascript Ninja. Así que estamos anulando todos los eventos ahora.
  if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
    const viejo = evento || ventana.evento;

    evento = {};
    // Clona el objeto antiguo para que podamos modificar los valores event = {};
    // A IE8 no le gusta cuando te metes con las propiedades de los eventos nativos
    // Firefox devuelve false para event.hasOwnProperty('type') y otras propiedades
    // lo que dificulta la copia.
    // HACER: Probablemente sea mejor crear una lista blanca de accesorios para eventos.
    for (clave const en antiguo) {
      // Safari 6.0.3 le advierte si intenta copiar la capa obsoleta X/Y
      // Chrome le advierte si intenta copiar el obsoleto keyboardEvent.keyLocation
      // y webkitMovementX/Y
      // Lighthouse se queja si se copia Event.path
      if (clave !== 'capaX' && tecla !== 'capaY' && clave !== 'ubicaciónclave' &&
          tecla !== 'webkitMovimientoX' && tecla !== 'webkitMovementY' &&
          tecla !== 'ruta') {
        // Chrome 32+ advierte si intenta copiar returnValue obsoleto, pero
        // todavía queremos hacerlo si preventDefault no es compatible (IE8).
        if (!(clave === 'returnValue' && old.preventDefault)) {
          evento[clave] = antiguo[clave];
        }
      }
    }

    // El evento ocurrió en este elemento
    if (!evento.objetivo) {
      evento.objetivo = evento.srcElement || documento;
    }

    // Manejar con qué otro elemento está relacionado el evento
    if (!evento.objetivorelacionado) {
      event.relatedTarget = event.fromElement === event.target ?
        evento.toElement :
        evento.fromElement;
    }

    // Detener la acción predeterminada del navegador
    event.preventDefault = function() {
      if (antiguo.prevenir por defecto) {
        old.preventDefault();
      }
      evento.returnValue = falso;
      antiguo.returnValue = falso;
      evento.defaultPrevented = verdadero;
    };

    evento.defaultPrevented = falso;

    // Evita que el evento burbujee
    event.stopPropagation = function() {
      if (antiguo.detenerPropagación) {
        old.stopPropagation();
      }
      event.cancelBubble = verdadero;
      old.cancelBubble = verdadero;
      event.isPropagationStopped = returnTrue;
    };

    event.isPropagationStopped = returnFalse;

    // Evita que el evento burbujee y ejecute otros controladores
    event.stopImmediatePropagation = function() {
      if (antiguo. detener la propagación inmediata) {
        old.stopImmediatePropagation();
      }
      event.isImmediatePropagationStopped = returnTrue;
      event.stopPropagation();
    };

    event.isImmediatePropagationStopped = returnFalse;

    // Manejar la posición del mouse
    si (evento.clienteX! == nulo && evento.clientX !== indefinido) {
      const doc = documento.documentElement;
      const cuerpo = documento.cuerpo;

      evento.paginaX = evento.clienteX +
        (doc && doc.scrollLeft || cuerpo && cuerpo.scrollLeft || 0) -
        (doc && doc.clienteIzquierdo || cuerpo && cuerpo.clienteIzquierdo || 0);
      evento.paginaY = evento.clienteY +
        (doc && doc.scrollTop || cuerpo && cuerpo.scrollTop || 0) -
        (doc && doc.clienteTop || cuerpo && cuerpo.clientTop || 0);
    }

    // Manejar pulsaciones de teclas
    evento.que = evento.charCode || evento.keyCode;

    // Botón de reparación para clics del mouse:
    // 0 == izquierda; 1 == medio; 2 == correcto
    if (evento.boton !== nulo && event.button !== indefinido) {

      // Lo siguiente está deshabilitado porque no pasa videojs-standard
      // y... ay.
      /* eslint-desactivar */
      evento.boton = (evento.boton & 1? 0 :
        (evento.botón & 4? 1:
          (evento.botón & 2? 2: 0)));
      /* habilitar eslint */
    }
  }

  evento.fijo_ = verdadero;
  // Devuelve la instancia arreglada
  evento de retorno;
}

/ **
 * Si se admiten detectores de eventos pasivos
 * /
let _supportsPassive;

const admite Pasivo = función () {
  if (typeof _supportsPassive !== 'booleano') {
    _supportsPassive = falso;
    intentar {
      const opts = Object.defineProperty({}, 'pasivo', {
        conseguir() {
          _supportsPassive = verdadero;
        }
      });

      ventana.addEventListener('prueba', nulo, opciones);
      ventana.removeEventListener('prueba', nulo, opciones);
    } catch (e) {
      // ignorar
    }
  }

  volver _supportsPassive;
};

/ **
 * Eventos táctiles que Chrome espera que sean pasivos
 * /
const eventos pasivos = [
  'inicio táctil',
  'tocar mover'
];

/ **
 * Agregar un detector de eventos al elemento
 * Almacena la función del controlador en un objeto de caché separado
 * y agrega un controlador genérico al evento del elemento,
 * junto con una identificación única (guid) para el elemento.
 *
 * @param {Elemento|Objeto} elemento
 * Elemento u objeto para vincular a los oyentes
 *
 * @param {cadena|cadena[]} tipo
 * Tipo de evento al que vincularse.
 *
 * @param {EventTarget~EventListener} fn
 * Oyente de eventos.
 * /
función de exportación en (elemento, tipo, fn) {
  if (Array.isArray(tipo)) {
    return _handleMultipleEvents(on, elem, type, fn);
  }

  si (!DomData.has(elem)) {
    DomData.set(elemento, {});
  }

  datos constantes = DomData.get(elem);

  // Necesitamos un lugar para almacenar todos nuestros datos de controlador
  si (! manejadores de datos) {
    manejadores de datos = {};
  }

  if (!data.handlers[tipo]) {
    manejadores de datos[tipo] = [];
  }

  si (!fn.guid) {
    fn.guid = Guid.nuevoGUID();
  }

  manejadores de datos[tipo].push(fn);

  if (! datos.despachador) {
    datos.deshabilitado = falso;

    data.dispatcher = función (evento, hash) {

      si (datos.deshabilitado) {
        devolver;
      }

      evento = fixEvent(evento);

      manejadores const = data.handlers[evento.tipo];

      si (manejadores) {
        // Copie los controladores, de modo que si se agregan/eliminan controladores durante el proceso, no se deshace de todo.
        const handlersCopy = handlers.slice(0);

        for (sea m = 0, n = handlersCopy.length; m < norte; m++) {
          if (event.isImmediatePropagationStopped()) {
            romper;
          } else {
            intentar {
              handlersCopy[m].call(elem, event, hash);
            } catch (e) {
              log.error(e);
            }
          }
        }
      }
    };
  }

  if (data.handlers[tipo].longitud === 1) {
    si (elem.addEventListener) {
      dejar opciones = falso;

      if (soporta Pasivo() &&
        eventos pasivos.indexOf(tipo) > -1) {
        opciones = {pasivo: verdadero};
      }
      elem.addEventListener(tipo, data.dispatcher, opciones);
    } más si (elem.attachEvent) {
      elem.attachEvent('on' + tipo, data.dispatcher);
    }
  }
}

/ **
 * Elimina detectores de eventos de un elemento
 *
 * @param {Elemento|Objeto} elemento
 * Objeto del que eliminar oyentes.
 *
 * @param {cadena|cadena[]} [tipo]
 * Tipo de oyente a eliminar. No incluyas para eliminar todos los eventos del elemento.
 *
 * @param {EventTarget~EventListener} [fn]
 * Oyente específico para eliminar. No incluir para eliminar oyentes de un evento
 * tipo.
 * /
función de exportación desactivada (elemento, tipo, fn) {
  // No quiero agregar un objeto de caché a través de getElData si no es necesario
  si (!DomData.has(elem)) {
    devolver;
  }

  datos constantes = DomData.get(elem);

  // Si no existen eventos, nada que desvincular
  si (! manejadores de datos) {
    devolver;
  }

  if (Array.isArray(tipo)) {
    return _handleMultipleEvents(off, elem, type, fn);
  }

  // Función de utilidad
  const removeType = function(el, t) {
    manejadores de datos[t] = [];
    _limpiarEventos(el, t);
  };

  // ¿Estamos eliminando todos los eventos vinculados?
  si (tipo === indefinido) {
    for (const t en data.handlers) {
      if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
        eliminarTipo(elemento, t);
      }
    }
    devolver;
  }

  manejadores const = data.handlers[tipo];

  // Si no existen controladores, nada que desvincular
  si (! manejadores) {
    devolver;
  }

  // Si no se proporcionó ningún oyente, elimine todos los oyentes para el tipo
  si (!fn) {
    removeType(elemento, tipo);
    devolver;
  }

  // Solo estamos eliminando un único controlador
  si (fn.guid) {
    para (sea n = 0; n < manejadores.longitud; n++) {
      if (controladores[n].guid === fn.guid) {
        manejadores.empalme(n--, 1);
      }
    }
  }

  _cleanUpEvents(elemento, tipo);
}

/ **
 * Activar un evento para un elemento
 *
 * @param {Elemento|Objeto} elemento
 * Elemento para desencadenar un evento en
 *
 * @param {EventTarget~Event|cadena} evento
 * Una cadena (el tipo) o un objeto de evento con un atributo de tipo
 *
 * @param {Objeto} [hash]
 * hash de datos para pasar junto con el evento
 *
 * @return {booleano|indefinido}
 * Devuelve lo contrario de `defaultPrevented` si el valor predeterminado era
 * prevenido. De lo contrario, devuelve `indefinido`
 * /
disparador de función de exportación (elemento, evento, hash) {
  // Obtiene datos del elemento y una referencia al padre (para burbujear).
  // No quiero agregar un objeto de datos al caché para cada padre,
  // así que revisa primero hasElData.
  const elemData = DomData.has(elem) ? DomData.get(elemento): {};
  const padre = elem.parentNode || elem.propietarioDocumento;
  // tipo = evento.tipo || evento,
  // manipulador;

  // Si el nombre de un evento se pasó como una cadena, crea un evento a partir de él
  if (tipo de evento === 'cadena') {
    evento = {tipo: evento, objetivo: elem};
  } más si (!evento.objetivo) {
    evento.objetivo = elemento;
  }

  // Normaliza las propiedades del evento.
  evento = fixEvent(evento);

  // Si el elemento pasado tiene un despachador, ejecuta los manejadores establecidos.
  si (elemData.despachador) {
    elemData.dispatcher.call(elem, evento, hash);
  }

  // A menos que se detenga explícitamente o que el evento no aparezca (p. ej., eventos multimedia)
  // llama recursivamente a esta función para hacer subir el evento en el DOM.
  si (padre && !event.isPropagationStopped() && evento.burbujas === verdadero) {
    trigger.call(null, padre, evento, hash);

  // Si está en la parte superior del DOM, activa la acción predeterminada a menos que esté deshabilitada.
  } más si (!padre && !event.predeterminadoEvitado && evento.objetivo && evento.objetivo[evento.tipo]) {
    if (!DomData.has(evento.objetivo)) {
      DomData.set(evento.objetivo, {});
    }
    const targetData = DomData.get(event.target);

    // Comprueba si el objetivo tiene una acción predeterminada para este evento.
    if (evento.objetivo[evento.tipo]) {
      // Deshabilita temporalmente el envío de eventos en el destino ya que ya hemos ejecutado el controlador.
      targetData.deshabilitado = verdadero;
      // Ejecuta la acción predeterminada.
      if (tipo de evento.objetivo[evento.tipo] === 'función') {
        evento.objetivo[evento.tipo]();
      }
      // Vuelve a habilitar el envío de eventos.
      targetData.deshabilitado = falso;
    }
  }

  // Informar al activador si se evitó el valor predeterminado devolviendo falso
  volver !evento.defaultPrevented;
}

/ **
 * Activar un oyente solo una vez para un evento.
 *
 * @param {Elemento|Objeto} elemento
 * Elemento u objeto a enlazar.
 *
 * @param {cadena|cadena[]} tipo
 * Nombre/tipo de evento
 *
 * @param {Evento~EventoListener} fn
 * Función de escucha de eventos
 * /
función de exportación uno (elemento, tipo, fn) {
  if (Array.isArray(tipo)) {
    return _handleMultipleEvents(uno, elemento, tipo, fn);
  }
  función const = función () {
    off(elemento, tipo, función);
    fn.apply(esto, argumentos);
  };

  // copia el guid a la nueva función para que pueda eliminarse usando el ID de la función original
  func.guid = fn.guid = fn.guid || Guid.nuevoGUID();
  on(elemento, tipo, función);
}

/ **
 * Activar un oyente solo una vez y luego apagarlo para todos
 * eventos configurados
 *
 * @param {Elemento|Objeto} elemento
 * Elemento u objeto a enlazar.
 *
 * @param {cadena|cadena[]} tipo
 * Nombre/tipo de evento
 *
 * @param {Evento~EventoListener} fn
 * Función de escucha de eventos
 * /
función de exportación cualquiera (elemento, tipo, fn) {
  función const = función () {
    off(elemento, tipo, función);
    fn.apply(esto, argumentos);
  };

  // copia el guid a la nueva función para que pueda eliminarse usando el ID de la función original
  func.guid = fn.guid = fn.guid || Guid.nuevoGUID();

  // varios complementos, pero uno solo para todo
  on(elemento, tipo, función);
}