margenn

Recently, I needed a cross-browser implementation of the getElementsByClassName Javascript DOM method. I couldn’t use a JS library like jQuery because code size had to be as small as possible.

Fortunately, Robert Nyman developed back in 2008 a very good function to handle this issue.

I have refactored the function definition logic, optimized code as much as possible, fixed some bugs and added the missing querySelectorAll DOM method.

I’ve written some QUnit tests if you want to test wether it works as it should or not.

Parameters:

className One or several class names, separated by space. Multiple class names demands that each match have all of the classes specified. Mandatory.

tag Tag name of the elements to match. Optional.

elm Reference to a DOM element to look amongst its children for matches. Recommended for better performance in larger documents. Optional.

ChangeLog:

7 Dic 2011

  • Added a refactored and bugfixed XPath method. Robert Nyman’s version did not add normalize-space(@class) (space normalization) so his method wouldn’t return elements with a class attribute containing space characters other than whitespace (newlines for example). Also, namespace try/catch was removed from his version since we’re dealing with HTML documents and therefore it’s not necessary.
  • BugFix: Some elements (nodes) may not have a getElementsByClassName method, so we test for this. As a fallback document will be used. See this issue for more info.
  • BugFix for IE: Array.slice cannot be called on StaticNodeLists in IE. Instead, a regular loop is used to populate the array.

So here it is my 2011 version of the getElementsByClassName function:

        
/*
 *  Refactored & improved in 2011 by Tubal Martin (http://www.margenn.com)
 *  Unit tests: http://www.margenn.com/tubal/javascript/getelementsbyclassname.html
 *  Original version developed by Robert Nyman in 2008 http://code.google.com/p/getelementsbyclassname/
 */	
var getElementsByClassName = document.getElementsByClassName ?
    function (className, tag, elm) {
        var nodeElm = elm && elm.getElementsByClassName ? elm : document,
            elements = nodeElm.getElementsByClassName(className),
            nodeName = tag ? new RegExp("\\b" + tag + "\\b", "i") : null,
            rElements = [],
            i = 0, l = elements.length,
            current;
        for (; i < l; i++) {
            current = elements[i];
            if (!nodeName || nodeName.test(current.nodeName)) {
                rElements.push(current);
            }
        }
        return rElements;
    } : document.querySelectorAll ?
    function (className, tag, elm) {
        var elements = (elm || document).querySelectorAll((tag || "") + "." + className.split(" ").join(".")),
            rElements = [], 
            i = 0, l = elements.length;
        for (; i < l; i++) {
            rElements.push(elements[i]);
        }
        return rElements;
    } : document.evaluate ?
    function (className, tag, elm) {
        var classes = className.split(" "),
            classesToCheck = "",
            rElements = [],
            i = 0, l = classes.length, 
            elements, node;
        for(; i < l; i++){
            classesToCheck += "[contains(concat(' ', normalize-space(@class), ' '), ' " + classes[i] + " ')]";
        }
        elements = document.evaluate(".//" + (tag || "*") + classesToCheck, (elm || document), null, 0, null);
        while (node = elements.iterateNext()) {
            rElements.push(node);
        }
        return rElements;
    } :
    function (className, tag, elm) {
        tag = tag || "*";
        elm = elm || document;
        var classes = className.split(" "),
            classesToCheck = [],
            elements = tag == "*" && elm.all ? elm.all : elm.getElementsByTagName(tag),
            rElements = [],
            i = j = 0, 
            il = classes.length, 
            jl = elements.length,
            current, match, k, kl;
        for (; i < il; i++) {
            classesToCheck.push(new RegExp("(^|\\s)" + classes[i] + "(\\s|$)"));
        }
        kl = classesToCheck.length;
        for (; j < jl; j++) {
            current = elements[j];
            match = false;
            for (k = 0; k < kl; k++) {
                match = classesToCheck[k].test(current.className);
                if (!match) {
                    break;
                }
            }
            if (match) {
                rElements.push(current);
            }
        }
        return rElements;
    };

Tags: cross-browser dom getElementsByClassName javascript class xpath evaluate

YUI 2.8 vs. Dojo 1.6 (I) - Calendario

Posted on March 19, 2011 by

A little bit about me

Me encanta Javascript! Mi compañero y socio Jano es quien mejor conoce cuánto me gusta. Ya desde mis inicios en el desarrollo web allá por 2006 cuando empezaba la revolución AJAX vi el potencial de dicho lenguaje para crear aplicaciones web altamente interactivas e incluso one-page-apps como Gmail. Además de tener potencial, el lenguaje me gustaba y era divertido jugar con él modificando una página, actualizando parte del contenido “por arte de magia” sin que la página sufriera un refresco, etc…

En dicha época (2005-2006) afortunadamente surgieron muchos proyectos cuyo objetivo era dotar a los desarrolladores de Javascript de herramientas que nos hicieran la vida más fácil a la hora de añadir interactividad y experiencia de usuario a una página o aplicación web.

Cuando por primera vez conocí jQuery, me pareció horrible…no era el javascript que yo conocía…me parecía alienígena y en un primer momento lo dejé de lado.

Un tiempo más tarde, jQuery no sólo ya no me parecía alienígena sino que lo consideraba fundamental para desarrollar un proyecto cross-browser y en una cuarta parte del tiempo.

Con el tiempo surgieron muchos frameworks, más de cien…pero eran menos de diez los favoritos y realmente buenos. De estos diez, dos llamaron mi atención: YUI y Dojo.

YUI y Dojo eran entonces y son hoy mucho más que una librería para manipular el DOM y realizar llamadas asíncronas con facilidad. Ofrecían y siguen ofreciendo un parque temático de widgets muy amplio y útil además de otras niceties como gestión de dependencias o internacionalización. Son Javascript frameworks con los que puedes construir una aplicación web de principio a fin sin utilizar librerías o plugins adicionales. No era ni es el caso de jQuery.

No quiero que se me entienda mal, me encanta jQuery pero para lo que fue concebido, ser la navaja suiza del DOM y aún sigue siendolo.

De modo que alucinado por lo que ofrecían tanto YUI como Dojo en aquel tiempo y la potencia de jQuery para manipular el DOM, tomé dos decisiones:

  • jQuery era (y sigue siendo) la mejor herramienta para manipular el DOM, mientras que YUI y Dojo ofrecían soluciones más pobres de modo que jQuery tenía que estar en mi arsenal.
  • YUI vs. Dojo: La decisión más difícil…probé ambos durante un tiempo y me convenció YUI por diferentes motivos:
    - Yahoo! 
    - Mejor documentación y organización
    - Comunidad mayor
    - Muy estable

Así que tenía dos herramientas sobre la mesa, jQuery y YUI y vi que ambas se complementaban. jQuery lo usaría para manipulación del DOM y gestión de los eventos y YUI para todo lo demás. So far, so good.

He pasado los últimos años utilizando esta solución mixta y con muy buenos resultados y experiencias pero desde el lanzamiento de Dojo 1.5 y la firme apuesta de Dojo por mejorar la documentación me he vuelto a replantear la pregunta ¿Qué herramienta(s) debo utilizar para el front-end development? Dojo 1.5 y esa pregunta han producido que escriba esta serie de artículos.

Como jQuery está para mi en otra liga distinta a la de YUI y Dojo, la cuestión es clara, ¿cuál elijo entre YUI y Dojo? y si elijo Dojo…¿podrá sustituir también a jQuery y convertirse en mi única herramienta? Si seguis esta serie conocereis mi decisión final.

Bien…y ¿cómo voy a comparar YUI y Dojo?

De la única manera posible, utilizando ambos head to head valorando qué ofrece cada uno y cuál resulta más productivo para las mismas tareas.

Hey…y ¿qué pasa con YUI 3?

YUI 3 lamentablemente está aún hoy bastante verde (incompleto y beta en su mayoría) como para ser comparado con YUI 2.8 o Dojo 1.6.

Se que YUI 2.x puede ser utilizado con YUI 3 para cubrir las carencias de este último pero no considero este hecho relevante ya que YUI 3 no es más que un rewrite completo de YUI 2, es decir, más de lo mismo pero con arquitectura, rendimiento y flexibilidad mejorados.

El calendario

En este primer artículo de la serie voy a comparar un widget que he utilizado bastante en los proyectos, el famoso calendario.

Os pondré en contexto, el objetivo del test o demo es asociar un calendario a un campo de texto de un formulario en el que el usuario debe introducir una fecha, de tal forma que cuando el usuario vaya a introducir la fecha, se despliegue un calendario para hacerle más cómoda la selección.

Sin más preámbulos echad un ojo al código fuente de la magnífica demo que he creado  :) y juguetead con ambas implementaciones del calendario antes de continuar leyendo.

Notas del desarrollo con Dojo:

  • Con 4 lineas de código terminado. 
  • El resultado es genial
  • HTML, CSS, comportamiento, localización (i18n), validación y advertencias visuales y localizadas en caso de fechas imposibles 100% automatizados.
  • He tardado 2 minutos
<form> 
    <fieldset> 
        <legend>Dojo calendar input</legend> 
        <label>Select a date:</label> 
        <input type="text" name="dojocalendar" data-dojo-type="dijit.form.DateTextBox" data-dojo-props="name:'dojocalendar',type:'text'"> 
        <input type="submit" value="Send"> 
    </fieldset> 
</form>          
dojo.require("dijit.form.DateTextBox");

Notas del desarrollo con YUI:

  • He necesitado crear HTML adicional (para poder mostrar el calendario) y asignar el atributo autocomplete=”off” al input para que el navegador no muestre un listado de fechas introducidas anteriormente por el usuario.
  • He necesitado crear estilos CSS adicionales.
  • He necesitado implementar todos los eventos de interacción del usuario con el campo de texto (focus porque no hay un indicador visual de que existen valores seleccionables, click para mostrar y ocultar el calendario y keyup para actualizar el calendario si el usuario escribe o cambia la fecha manualmente) y el documento (ocultar el calendario al hacer click tanto fuera del campo de texto como del calendario) para igualar el comportamiento de Dojo.
  • He necesitado localizar el calendario a mi idioma traduciendo las cadenas de texto del mismo e indicando aspectos como qué día de la semana es el primero en aparecer en cada semana de un calendario (Lunes, no Domingo).
  • He necesitado formatear la fecha devuelta por el calendario de dos formas: una de un modo humano y localizado para mostrarsela al usuario y otra en el formato literal date en SQL (como Dojo).
  • He necesitado bastantes más lineas de código.
  • El resultado es bueno, pero no tan bueno como el de Dojo ya que este incluso valida la fecha introducida por un usuario y si es una fecha imposible le alerta visual y descriptivamente además de en su localización. Para igualar el mismo comportamiento en YUI necesitaría un icono, rutina de comprobación de fecha válida, el widget para crear tooltips, el mensaje localizado a mostrar y más HTML y CSS.
  • He necesitado más de 30 minutos.
<form> 
    <fieldset> 
        <legend>YUI calendar input</legend> 
        <label>Select a date:</label> 
        <div class="yuicalendar-popup-wrapper"> 
            <input type="text" id="yuicalendarinput" autocomplete="off"> 
            <input type="hidden" id="yuicalendarhiddeninput" name="yuicalendar"> 
            <div id="yuiCalendarContainer"></div> 
        </div> 
        <input type="submit" value="Send"> 
    </fieldset> 
</form>  
(function(y){
    var initYUI = function(){
        var yue = y.util.Event,
            yud = y.util.Dom,
            yuiCalContainer = yud.get('yuiCalendarContainer'),
            yuiCalInput = yud.get('yuicalendarinput'),
            yuiCalHiddenInput = yud.get('yuicalendarhiddeninput'),
            
            setDate = function(type, args, obj) {
                var date = args[0][0], 
                    year = date[0], 
                    month = date[1], 
                    day = date[2],
                    userDate = (day < 10 ? "0" + day : day) + "/" + (month < 10 ? "0" + month : month) + "/" + year;

                yuiCalInput.value = userDate;
                updateInputDate(userDate);
                yuiCalendar.hide();
            },
            
            getDateFields = function(userDate){
                return userDate.split("/");
            },
            
            updateInputDate = function(userDate){
                var dateFields = getDateFields(userDate);
                yuiCalHiddenInput.value = dateFields[2] + "-" + dateFields[1] + "-" + dateFields[0];
            },
            
            updateCalendar = function(e){
                var month, year, dateFields, userDate = this.value;

                if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(userDate)) {
                    dateFields = getDateFields(userDate);
                    month = dateFields[1];
                    year = dateFields[2];
                    updateInputDate(userDate);
                    yuiCalendar.cfg.setProperty("selected", userDate, false);
                    yuiCalendar.cfg.setProperty("pagedate", month + "/" + year, false);
                    yuiCalendar.render();
                }
            },
            
            yuiCalendar = new y.widget.Calendar("yuiCalendarContainer", { 
                locale_weekdays: "1char",
                navigator: {
                    strings : { 
                        month: "Selecciona un Mes", 
                        year: "Introduce un Año", 
                        submit: "Hecho", 
                        cancel: "Cancelar", 
                        invalidYear: "Por favor, introduce un año válido" 
                    }
                },
                start_weekday: 1,
                MDY_DAY_POSITION: 1,
                MDY_MONTH_POSITION: 2,
                MDY_YEAR_POSITION: 3,
                MONTHS_LONG: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
                WEEKDAYS_1CHAR: ["D", "L", "M", "M", "J", "V", "S"]
            });
         
        yuiCalendar.selectEvent.subscribe(setDate);
        yuiCalendar.render();
        
        yue.on("yuicalendarinput", "keyup", updateCalendar);
        
        yue.on("yuicalendarinput", "focusin", function(e) {
            this.className = "focused";
            yuiCalendar.show(); 
        });
        
        yue.on("yuicalendarinput", "click", function(e) {
            if (this.className == "focused") {
                this.className = "";
                return;
            }
            
            if (yuiCalContainer.style.display == "block") {
                yuiCalendar.hide();
            } else {
                yuiCalendar.show();
            }
        });
        
        yue.on(document, "click", function(e) {
            var el = yue.getTarget(e);

            if (el != yuiCalInput && el != yuiCalContainer && !yud.isAncestor(yuiCalContainer, el)) {
                yuiCalendar.hide();
            }
        });
    };
    
    (new y.util.YUILoader({
        require: ["calendar"],
        loadOptional: false,
        onSuccess: initYUI,
        timeout: 10000,
        combine: false
    })).insert();
}(YAHOO));

Bueno, esas son mis notas de desarrollo…sin duda alguna Dojo ofrece muuuchas más ventajas sobre YUI en este escenario tan real y típico como la vida misma. Dojo cubre de forma automatizada y productiva todas las necesidades típicas en este escenario y por ello le doy un 10.

Por último he comparado las opciones que nos da cada framework en cuanto a usos del calendario más allá del típico (flexibilidad):

  • Con YUI podemos crear grupos de calendarios (Multi-Page)
  • Con YUI podemos seleccionar varias fechas en un mismo calendario (Multi-Select)
  • Todo lo demás puede hacerse con ambos frameworks de un modo u otro.

Tanto el Multi-Page como el Multi-Select de YUI me parecen de bastante poca utilidad la verdad. No he encontrado hasta la fecha un escenario donde haya necesitado dichas soluciones.

Conclusión

YUI 0 - 1 Dojo

Dojo ha demostrado ser mejor en todos los aspectos en esta comparación, dejando por los suelos a YUI.

¿Y vosotros qué opinais? ¿Con cuál os quedariais?

Espero que os haya gustado mi análisis y la demo, en el próximo combate entre YUI y Dojo analizaré otro widget, el autocomplete.

Stay tuned!!

Tags: yui dojo calendar calendario javascript framework widget

Función “slice” de Javascript para PHP

Posted on December 15, 2010 by

Recientemente me embarqué en un proyecto personal (que daré a conocer dentro de poco) que implicaba reescribir en PHP código escrito previamente en Javascript.

Al poco de empezar la tarea pude observar que el script dependía de la función slice de Javascript. Miré en la documentación de PHP y ninguna función hacía lo mismo. Consulté las funciones para los arrays…y tampoco ninguna me servía. Finalmente busqué en Google y sólo encontré gente preguntando en foros si existía dicha función en PHP o algún port pero ninguna solución.

Así que me puse manos a la obra, al cabo de un rato escribí los tests y ¡Tachán! os presento la implementación en PHP de la función slice de Javascript para strings exclusivamente ya que el proyecto no requería tratar con arrays.

El comportamiento es idéntico a su “counterpart” en Javascript, de modo que si alguien necesita cambiar qué devuelve la función en caso de una excepción (“” string vacío) tendrá que modificar el código y crear sus tests.

/** 
* PHP port of Javascript's "slice" function
* Author: Tubal Martin http://margenn.com
* Tests: http://margenn.com/tubal/str_slice/
*
* @param string $str
* @param int    $start index
* @param int    $end index (optional)
*/
function str_slice($str, $start, $end = FALSE)
{
    if ($start < 0 || $end <= 0) {
        
        if ($end === FALSE) {
            $slice = substr($str, $start);
            return ($slice === FALSE) ? '' : $slice;
        }
        
        $max = strlen($str);
        
        if ($start < 0) {
            if (($start = $max + $start) < 0) {
                return '';
            }
        }
    
        if ($end < 0) {
            if (($end = $max + $end) < 0) {
                return '';
            }
        }
        
        if ($end <= $start) {
            return '';
        }
    }   
    
    $slice = substr($str, $start, $end - $start);
    return ($slice === FALSE) ? '' : $slice;
}

Podéis ejecutar los tests visitando la URL indicada en el comentario situado en la cabecera de la función.

La función está optimizada para un rendimiento óptimo.

Espero que os sirva de utilidad ;)

Tags: slice javascript php port function