Francute / Kongregate - AutoCompletar nombres

// ==UserScript==
// @namespace     http://openuserjs.org/users/Francute
// @name          Kongregate - AutoCompletar nombres
// @description   En el chat de la página, simplemente escribir parte de un nombre de usuario y autocompletar con Tab. Shif + Tab para volver a la recomendación anterior.
// @copyright     Francute
// @license       MIT
// @version       0.15
// @include       http://www.kongregate.com/games/*/*
// ==/UserScript==

// ==OpenUserJS==
// @author        Francute
// ==/OpenUserJS==


(function() {
    'use strict';

    class Chat {

        constructor(selectorContenedorDelChat, selectorContenedorDeNodosUsuarios, selectorTextArea) {
            this.pestanasChat = [];

            this.nodoContenedorDeChats = document.querySelector(selectorContenedorDelChat);

            this.selectorContenedorDeUsuarios = selectorContenedorDeNodosUsuarios;
            this.selectorEntrada = selectorTextArea;

            this.cargarPestanas();
        }

        cargarPestanas() {

            let self = this;

            new MutationObserver(function(mutaciones) {
                mutaciones.forEach(function(mutacion) {
                    if (seAgregaronChats(mutacion)) {
                        mutacion.addedNodes.forEach(nodoChatNuevo => this.cargarNuevaPestana(nodoChatNuevo));
                    } else if (seRemovieronChats(mutacion)) {
                        mutacion.removedNodes.forEach(nodoChatEliminado => this.eliminarPestana(nodoChatEliminado));
                    }
                }, self);
            }).observe(this.nodoContenedorDeChats, {
                childList: true
            });

            function seAgregaronChats(mutacion) {
                return mutacion.addedNodes.length > 0;
            }

            function seRemovieronChats(mutacion) {
                return mutacion.removedNodes.length > 0;
            }

        }

        cargarNuevaPestana(nodoChat) {
            this.pestanasChat.push(new ComponentePestana(nodoChat, this.selectorContenedorDeUsuarios, this.selectorEntrada));
        }

        eliminarPestana(nodoChat) {
            let indicePestanaEncontrada;
            let seEncontroPestana = this.pestanasChat.some(function(pestana, indice) {
                indicePestanaEncontrada = indice;
                return pestana.nodoChat == nodoChat;
            });

            if (seEncontroPestana) {
                this.pestanasChat.splice(indicePestanaEncontrada, 1);
            }
        }

        cargarPestanasChats(selectorContenedorDeNodosUsuarios, selectorTextArea) {
            this.pestanasChat = [];
            Array.from(this.nodoContenedorDeChats.children)
                .forEach(nodoChat => this.pestanasChat.push(new ComponentePestana(nodoChat, selectorContenedorDeNodosUsuarios, selectorTextArea)));
        }

    }

    class ComponentePestana {
        constructor(nodo, selectorContenedorDeNodosUsuarios, selectorTextArea) {
            this.nodoChat = nodo;

            this.usuarios = new ComponenteUsuarios(nodo.querySelector(selectorContenedorDeNodosUsuarios));
            this.nodoEntrada = new ComponenteEntrada(this.usuarios, nodo.querySelector(selectorTextArea));
        }

    }

    class ComponenteUsuarios {

        constructor(nodoUsuarios) {
            this.posiblesUsuarios = [];
            this.posicionUsuarioActual = -1;

            this.nodoUsuarios = nodoUsuarios;

            this.actualizarListaDeUsuarios();
        }

        actualizarListaDeUsuarios() {
            this.usuarios = Array.from(this.nodoUsuarios.querySelectorAll("span > span.username"))
                .map(nodo => nodo.innerHTML)
                .sort((a, b) => (a.toLowerCase() > b.toLowerCase()) ? 1 : -1);
        }

        buscarPosiblesUsuarios(parteDelNombre) {
            this.actualizarListaDeUsuarios();
            this.posiblesUsuarios =
                this.usuarios.filter(usuario => usuario.search(new RegExp(parteDelNombre + "\\w*$", "i")) === 0)
                .sort((a, b) => (a.toLowerCase() > b.toLowerCase()) ? 1 : -1);
            this.posicionUsuarioActual = -1;
            return this.posiblesUsuarios;
        }

        esUnUsuarioYaDevuelto(nombreUsuario) {
            return (this.posiblesUsuarios.indexOf(nombreUsuario) != -1);
        }

        existenUsuariosQueConcuerdenCon(parteDelNombreDeUsuario) {
            return this.buscarPosiblesUsuarios(parteDelNombreDeUsuario).length > 0;
        }

        siguiente() {
            return this.posiblesUsuarios[(++this.posicionUsuarioActual) % this.posiblesUsuarios.length];
        }

        anterior() {
            if (this.posicionUsuarioActual <= 0) {
                this.posicionUsuarioActual = this.posiblesUsuarios.length;
            }
            return this.posiblesUsuarios[--this.posicionUsuarioActual % this.posiblesUsuarios.length];
        }

        finalizarBusqueda() {
            if (this.posiblesUsuarios.length > 0) this.posiblesUsuarios = [];
        }

    }

    class ComponenteEntrada {

        constructor(usuarios, nodoEntrada) {
            this.usuarios = usuarios;
            this.entrada = nodoEntrada;
            this.separador = " ";

            this.activarEventos();
        }

        activarEventos() {
            /*  Bugs pronosticados:
            Escenario 1:
                Busco por un usuario cuyo nombre comience con "la", y encuentra a "ladillaFeliz" y "LaEraWakeista".
                Se conecta un usuario "laxvio".
                Si continúo haciendo Tab sin buscar de nuevo, no podré encontrar a "laxvio".
                    -¿Posible solución? Dejar Kong.
            Escenario 2:
                Busco un usuario que justo se desconectó, no lo voy a poder encontrar. ¿Será un bug realmente?
            Escenario 3:
                Busco por un usuario cuyo nombre comience con "la", y encuentra a "ladillaFeliz" y "laxvio".
                Selecciono alguno de los 2 y sigo escribiendo en el chat con normalidad.
                Escribo luego "laxvio" y al tocar Tab debería cambiarla por "ladillaFeliz". 
                - Se puede resolver fácil actualizando cuando toque algo diferente de Shift o Tab, pero...
                    Doy posibilidades a bugs que se desprenden de este?
        */

            this.entrada.addEventListener("keydown", function(evento) {

                if (evento.code === 'Tab') {
                    evento.preventDefault();

                    if (this.usuarios.esUnUsuarioYaDevuelto(this.traerUltimaPalabra())) {
                        this.borrarUltimaPalabra();
                        if (seEstaPresionandoLaTeclaShift()) {
                            this.agregarAlMensaje(this.usuarios.anterior());
                        } else {
                            this.agregarAlMensaje(this.usuarios.siguiente());
                        }
                    } else {
                        if (this.usuarios.existenUsuariosQueConcuerdenCon(this.traerUltimaPalabra())) {
                            this.borrarUltimaPalabra();
                            this.agregarAlMensaje(this.usuarios.siguiente());
                        }
                    }

                }

                function seEstaPresionandoLaTeclaShift() {
                    return evento.shiftKey;
                }

            }.bind(this));

        }

        traerPalabrasActualmenteIngresadas() {
            this.modeloTextArea = this.entrada.value.trim().split(this.separador);
            return this.modeloTextArea;
        }

        traerUltimaPalabra() {
            let palabrasIngresadas = this.traerPalabrasActualmenteIngresadas();
            return palabrasIngresadas[palabrasIngresadas.length - 1];
        }

        borrarUltimaPalabra() {
            this.modeloTextArea.pop();
        }

        agregarAlMensaje(texto) {
            this.entrada.value = (
                this.modeloTextArea.join(this.separador) +
                this.separador +
                texto
            ).trim();
        }

    }

    document.observe('holodeck:ready', function() {
        new Chat("#chat_rooms_container", ".users_in_room", ".chat_input");
    });

})();