De una lista aburrida, a una lista organizada.

Lo que estoy a punto de mostrarles es, en mi opinión, lo que separa a javascripters de jQuery kiddies.

Hace ya un tiempo un cliente se me acercó con un problema. Sinceramente, el problema tiene mucho que ver con una mala elección de software para correr su proyecto. El cliente usa un servidor mac, que ya viene con un servicio de wiki. El cliente usa este servicio de wiki para hacer su manual de helicópteros. Hasta ahí todo va bien. El problema viene cuando el cliente quiere construir una lista de contenidos de su manual. Como el sitio corre en una wiki, no hay forma de hacer tal cosa. Además, tomemos en cuanta que esto es cacharro de esos que hace apple. En otras palabras una concha bien cerrada. Y si a eso le agregamos mi falta de conocimiento sobre dicha concha, tenemos un desastre perfecto.

El cliente sugiere esto:

Usar el widget donde pone hot topics para crear la lista de contenidos. El widget funciona en base a una categoría. Uno almacena los artículos en la categoría hot, y estos se ponen automáticamente en el widget en una columna del sitio. Hasta ahí todo bien. El problema empieza a surgir cuando el cliente me dice que los contenidos son escritos por diferentes personas y no necesariamente en orden, por lo que en la lista aparece algo así:

1 title
5 title
9 title
1.1 title
4 title
...

Es un espaggeti.

Ahí es donde entro yo. El cliente me lava el coco diciendo que soy un genio y que mi trabajo siempre le ha encantado y termina diciendo que sería bueno si yo puedo usar Javascript para ordenar esa lista, pero eso no es todo, el cliente quiere que sea una lista con listas anidadas y desplegables. Es decir, ir del espaggeti anterior a esto:

1 title
   1.1 title
2 title
   2.1 title
   2.2 tite
      2.2.1 title
4 title
...

Notar que la falta de un 3 no es que se me ha olvidado o que no se contar, sino que representa que alguien ha sido flojo y no ha escrito el capítulo 3 del manual.

Pues ese es el reto, y todo lo tengo que hacer con javascript y que qude bonito, sea desplegable y cargue rápido. Además, alguien no escribió el capítulo 3, pero alguien más si escribió el 3.2.1. Ahí lo tienen. Quiero que un jQuery me haga esto por favor. (se ríe burlónamente…)

Claro, no he posteado aquí con esperanza de que un jQuery kiddy me resuelva el problema. No explicaré el código, pero se los preseneto acá para que lo examinen, me critiquen y me digan que se puede mejorar.

Un par de cosas que deben saber:

w es atajo para window
B es mi objeto global. Sirve para mantener mi código lo más encapsulado posible.
d es atajo para document.
Hay un máximo de 4 niveles por cada tema. Es decir, que solo puede haber 1.1.1.1 pero no 1.1.1.1.1
Este no es todo el código, por lo que quizá haya referencia a partes del código que no se encuentran en lo que les estoy mostrando. Para que se den una idea, está viendo 188 de un total de 373 líneas.

w.B.hhNester {
    allIndexes: null,
    ul: null,
    initialElem: null,
    unsorted: [],
    init: function (id) {
        var ul = d.getElementById(id);
        if (!ul) {
            return false;
        }
        if (ul.nodeName != "UL") {
            return false; //fix a problem where calendar section would just say print all (Safari, Chrome, IE)
        }
        var lis = ul.getElementsByTagName('li');
        if (!lis) {
            return false;
        }

        this.initialElem = ul;
        this.doNesting(lis);
    },
    doNesting: function (lis) {
        if (!lis) {
            return false;
        }
        var len = lis.length;
        var allIndexes = {};
        var topLevels = [];
        var secondLevels = [];
        var thirdLevels = [];
        var fourthLevels = [];
        for (var i = 0; i < len; i++) {
            var li = lis[i];
            var theA = li.getElementsByTagName('a')[0];
            theNum = theA.innerHTML.split(' ')[0];
            allIndexes[theNum] = li;
            var tmpArr = theNum.split('.');
            switch (tmpArr.length) {
            case 1:
                topLevels.push(theNum);
                break;
            case 2:
                secondLevels.push(theNum);
                break;
            case 3:
                thirdLevels.push(theNum);
                break;
            case 4:
                fourthLevels.push(theNum);
                break;
            }
        }
        this.allIndexes = allIndexes;

        function sortNumber(a, b) {
            var r;
            if (!isNaN(a) && !isNaN(b)) {
                r = a - b;
            } else if (isNaN(a)) {
                r = 1;
            } else if (isNaN(b)) {
                r = -1;
            }
            return r;
        }
        topLevels.sort(sortNumber);
        secondLevels.sort(sortNumber);
        thirdLevels.sort(function (a, b) {
            var aarr = a.split('.');
            var barr = b.split('.');
            return aarr[2] - barr[2];
        });
        fourthLevels.sort(function (a, b) {
            var aarr = a.split('.');
            var barr = b.split('.');
            return aarr[3] - barr[3];
        });
        this.buildPrimaryUl(topLevels);
        this.buildSecondaryLevel(secondLevels);
        this.buildThirdLevel(thirdLevels);
        this.buildFourthLevel(fourthLevels);
        this.buildUnsorted();
        B.listDropper.init(this.ul);
        this.initialElem.parentNode.replaceChild(this.ul, this.initialElem);
        this.initialElem = null;
        B.printAll.init(this.ul);
    },
    buildPrimaryUl: function (topLevelsArr) {
        var ul = d.createElement('ul');
        this.ul = ul;
        this.ul.subIndex = [];
        for (var i = 0, len = topLevelsArr.length; i < len; i++) {
            this.ul.subIndex[topLevelsArr[i] - 1] = this.allIndexes[topLevelsArr[i]];
            ul.appendChild(this.allIndexes[topLevelsArr[i]]);
        }
    },
    buildSecondaryLevel: function (secondLevelArr) {
        for (var i = 0, l = secondLevelArr.length; i < l; i++) {
            var belongsTo = parseInt(secondLevelArr[i]) - 1;
            var theNode = this.ul.subIndex[belongsTo];
            if (!theNode) {
                this.unsorted.push(secondLevelArr[i]);
                continue;
            }
            var hasUl = theNode.getElementsByTagName('ul');
            if (hasUl.length == 0) {
                theNode.appendChild(d.createElement('ul'));
            }
            if (!theNode.subIndex) {
                theNode.subIndex = [];
            }
            var tmpArr = secondLevelArr[i].split('.');
            theNode.subIndex[tmpArr[1] - 1] = this.allIndexes[secondLevelArr[i]];
            theNode.getElementsByTagName('ul')[0].appendChild(this.allIndexes[secondLevelArr[i]]);
        }
    },
    buildThirdLevel: function (thirdLevelsArr) {
        for (var i = 0, l = thirdLevelsArr.length; i < l; i++) {
            var theNum = thirdLevelsArr[i];
            var tmpArr = theNum.split('.');
            var topClasifier = tmpArr[0] - 1;
            var secondaryClasifier = tmpArr[1] - 1;
            var belongsToTop = this.ul.subIndex[topClasifier];
            if (!belongsToTop) {
                this.unsorted.push(theNum);
                continue;
            }
            var belongsToSecond = belongsToTop.subIndex[secondaryClasifier];
            if (!belongsToSecond) {
                this.unsorted.push(theNum);
                continue;
            }
            var hasUl = belongsToSecond.getElementsByTagName('ul');
            if (hasUl.length == 0) {
                var newUl = d.createElement('ul');
                belongsToSecond.appendChild(newUl);
            }
            if (!belongsToSecond.subIndex) {
                belongsToSecond.subIndex = [];
            }
            belongsToSecond.subIndex[tmpArr[2] - 1] = this.allIndexes[theNum];
            belongsToSecond.getElementsByTagName('ul')[0].appendChild(this.allIndexes[theNum]);
        }
    },
    buildFourthLevel: function (fourthLevelsArr) {
        for (var i = 0, l = fourthLevelsArr.length; i < l; i++) {
            var theNum = fourthLevelsArr[i];
            var tmpArr = theNum.split('.');
            var topClasifier = tmpArr[0] - 1;
            var secondaryClasifier = tmpArr[1] - 1;
            var thirdClasifier = tmpArr[2] - 1;
            var belongsToTop = this.ul.subIndex[topClasifier];
            if (!belongsToTop) {
                this.unsorted.push(theNum);
                continue;
            }
            var belongsToSecond = belongsToTop.subIndex[secondaryClasifier];
            if (!belongsToSecond) {
                this.unsorted.push(theNum);
                continue;
            }
            var belongsToThird = belongsToSecond.subIndex[thirdClasifier];
            if (!belongsToThird) {
                this.unsorted.push(theNum);
                continue;
            }
            var hasUl = belongsToThird.getElementsByTagName('ul');
            if (hasUl.length == 0) {
                var newUl = d.createElement('ul');
                belongsToThird.appendChild(newUl);
            }
            belongsToThird.getElementsByTagName('ul')[0].appendChild(this.allIndexes[theNum]);
        }
    },
    buildUnsorted: function () {
        if (this.unsorted.length > 0) {
            var ul = this.ul;
            var li = d.createElement('li');
            li.appendChild(d.createTextNode('Unsorted Elements:'));
            ul.appendChild(li);
            var ul2 = d.createElement('ul');
            for (var i = 0, l = this.unsorted.length; i < l; i++) {
                ul2.appendChild(this.allIndexes[this.unsorted[i]]);
            }
            li.appendChild(ul2);
        }
    }
}

Algunos de los puntos que creo importante mencionar son:

Notar el poco uso de loops para ordenar los elementos. En su lugar he usado arrays. Hay un solo loop que recorre todos lo elementos y 5 que recorren un pequeño set de elementos. Si la matemática no me falla, cada elemento es recorrido solo dos veces por un loop.

LA función sortNumber fue particularmente interesante de definir. Inicialmente era solo:

function sortNumber(a, b) {
   return  a - b;
}

Hasta que el cliente decidió no seguir las reglas para nombrar sus títulos y empezó a meter títulos sin número al inicio. En ese momento hubo que intervenir y lograr que el script decidiera si estaba tratando con números o letras. Entonces la función se tornó en:

function sortNumber(a, b) {
    var r;
    if (!isNaN(a) && !isNaN(b)) {
        r = a - b;
    } else if (isNaN(a)) {
        r = 1;
    } else if (isNaN(b)) {
        r = -1;
    }
    return r;
}

Que básicamente logra ordenar los elementos aun cuando se encuentre con letras en lugar de números.

El script no es completamente fool proof. Por ejemplo, si alguien le pone de título a una sección “Es.te.es mi titulo”, el scrip fallaría rotundamente, pero en serio quién en su sano juicio haría eso?

Bueno, espero que examinar este código les ayude de algo. Es pequeñas cosas como esta a las que me refiero cuando digo que ser programador no es simplemente conocer el lenguaje, es toda una forma de pensar diferente.

2 thoughts on “De una lista aburrida, a una lista organizada.

  1. Impecable el post. Coincido totalmente con vos, en especial con el último párrafo.

    Espero sinceramente que sigas teniendo tiempo para compartir con los demás, tu experiencia en esta disciplina llamada programación.

    • Pues yo tambien espero poder seguir actualizando el blog. Por ahora tengo un par de borradores que necesitan ser pulidos y una serie en animacion con javascript.

Comments are closed.