/* -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
*/
;(function($) {
    var DEFAULT_EXPR = ":input:visible";

    $.resetTabindex = function(expr) {
        // Par défaut, prendre tous les champs de saisie *visibles*
        if (expr === undefined) expr = DEFAULT_EXPR;
        // Réinitialiser tabindex
        $(expr).each(function() {
            $(this).attr("tabindex", 0);
        });
    };
    $.initTabindex = function(expr) {
        // Par défaut, prendre tous les champs de saisie *visibles*
        if (expr === undefined) expr = DEFAULT_EXPR;
        // D'abord calculer la plus grande valeur de tabindex
        var tabindex = 0;
        var $expr = $(expr);
        $expr.each(function() {
            var ti = parseInt($(this).attr("tabindex"));
            if (ti > tabindex) tabindex = ti;
        });
        // Puis initialiser les éléments restants à partir de tabindex + 1
        tabindex += 1;
        $expr.each(function() {
            var ti = parseInt($(this).attr("tabindex"));
            if (!ti) $(this).attr("tabindex", tabindex++);
        });
    };
    $.updateTabindex = function(expr) {
        if (expr === undefined) {
            $.resetTabindex(":input");
            $.initTabindex(DEFAULT_EXPR);
        } else {
            $.resetTabindex(expr);
            $.initTabindex(expr);
        }
    };
    // éléments de formulaires navigables avec la touche Entrée
    $.NAVIGATE_INCLUDES = ":input";
    // éléments de formulaire qui ne sont pas navigables avec la touche Entrée,
    // à exclure de la liste sélectionnée avec $.NAVIGATE_INCLUDES
    $.NAVIGATE_EXCLUDES = "textarea, :submit, :reset, :button";
    $.navigate = function(expr, selectAfterFocus, initTabindex) {
        // Par défaut, prendre tous les champs de saisie *visibles*
        if (expr === undefined) expr = DEFAULT_EXPR;
        // Faut-il sélectionner le champ après avoir changé le focus?
        if (selectAfterFocus === undefined) selectAfterFocus = true;
        // Faut-il initialiser tabindex pour les éléments sélectionnés? Sinon,
        // il FAUT spécifier les valeurs de tabindex pour tous les champs
        // concernés par la navigation
        if (initTabindex === undefined) initTabindex = true;

        // Dans les fonctions suivantes, input est un objet non wrappé par
        // jQuery. $input est le même objet wrappé par jQuery.
        var isdefined = function(value) { return value !== null && value !== undefined; }
        var isempty = function(input) { return $(input).val().length == 0; }
        var hasSelection = function(input) {
            // Tester si l'élément peut contenir une sélection. Typiquement,
            // il s'agit d'un champ de saisie de texte.
            try {
                return isdefined(input.selectionStart)
            } catch (e) {
            }
        }
        var isNavigable = function(input) {
            // Tester si l'élément est navigable avec la touche Entrée
            // Par défaut, on prend tous les éléments de type :input sauf textarea et submit
            var $input = $(input);
            return $input.is($.NAVIGATE_INCLUDES) && !$input.is($.NAVIGATE_EXCLUDES);
        }
        var noSel = function(input) {
            return input.selectionStart == input.selectionEnd;
        }
        var allSel = function(input) {
            var length = $(input).val().length;
            return length > 0 && input.selectionStart == 0 && input.selectionEnd == length;
        }
        var noSelOrAllSel = function(input) {
            return noSel(input) || allSel(input);
        }
        var focusinput = function(tabindex, select) {
            if (tabindex > 0) {
                var $input = $("[tabindex='" + tabindex + "']");
                $input.focus();
                if (select) $input.select();
            }
        }
        var previnput = function(input, select) {
            var tabindex = $(input).attr("tabindex");
            if (tabindex) focusinput(parseInt(tabindex) - 1, select);
        }
        var nextinput = function(input, select) {
            var tabindex = $(input).attr("tabindex");
            if (tabindex) focusinput(parseInt(tabindex) + 1, select);
        }
        var atbegin = function(input) {
            if (! hasSelection(input)) return true;
            else if (noSelOrAllSel(input)) return input.selectionStart == 0;
            else return false;
        }
        var atend = function(input) {
            var length = $(input).val().length;
            if (! hasSelection(input)) return true;
            else if (noSelOrAllSel(input)) return input.selectionEnd == length;
            else return false;
        }
        var hideAutocompleter = function(input) {
            var select = $(input).data("ac.select");
            if (select) select.hide();
        }

        if (initTabindex) $.initTabindex(expr);
        // Installer les handlers pour backspace, entrée, flèches gauche et droite.
        var state = {
            'autocomplete': false,
            'leftKeyThenPrevinput': false,
            'rightKeyThenNextinput': false,
        };
        $(expr).keypress(function (e) {
            if (state.autocomplete) {
                // Si on est dans l'état auto-complétion, ne pas traiter les
                // caractères pour éviter les conflits
                state.autocomplete = false;
            } else if (e.which == 8 && hasSelection(this) && isempty(this)) {
                // backspace sur un champ vide déplace la sélection sur le champ précédent
                hideAutocompleter(this);
                previnput(this);
                return false;
            } else if (e.which == 13) {
                if (e.ctrlKey) {
                    if ($.wosubmit) {
                        $.wosubmit(this);
                        return false;
                    }
                } else if (isNavigable(this)) {
                    // entrée sur un champ déplace la sélection sur le champ suivant
                    nextinput(this, selectAfterFocus);
                    return false;
                }
            }
        }).keydown(function (e) {
            // intercepter flèches gauche et droite pour ne pas avoir d'effet de bord qui
            // gênerait le traitement des événements dans keyup
            var key = e.which;
            if (key == 37 && atbegin(this) && !allSel(this)) {
                state.leftKeyThenPrevinput = true;
                return false;
            } else if (key == 39 && atend(this) && !allSel(this)) {
                state.rightKeyThenNextinput = true;
                return false;
            }
        }).keyup(function (e) {
            var key = e.which;
            if (state.leftKeyThenPrevinput) {
                // flèche gauche au début du champ déplace la sélection sur le champ précédent
                state.leftKeyThenPrevinput = false;
                hideAutocompleter(this);
                previnput(this, selectAfterFocus);
            } else if (state.rightKeyThenNextinput) {
                // flèche droite à la fin du champ déplace la sélection sur le champ suivant
                state.rightKeyThenNextinput = false;
                hideAutocompleter(this);
                nextinput(this, selectAfterFocus);
            } else if (key == 40 || (state.autocomplete && key == 38)) {
                // flèche basse: on va sur un élément de l'auto-complétion.
                // Si on est déjà dans l'état auto-complétion, la flèche haute
                // est reconnue aussi
                state.autocomplete = true;
            } else {
                // Toute autre touche désactive le mode auto-complétion
                state.autocomplete = false;
            }
        }).attr("autocomplete", "off");
    };

    // Activer le focus et placer le curseur à la fin de la ligne spécifiée.
    // Par défaut, il s'agit de la première ligne (soit line==0)
    // Si setSelectionRange n'est pas supporté, c'est un NOP
    $.fn.cursorAtEOFL = function(line) {
        if (line === undefined) line = 0;
        return this.each(function() {
            var $this = $(this);
            if (this.setSelectionRange) {
                var text = $this.val();
                var pos = 0;
                while (line-- >= 0) {
                    var index = text.search('\r?\n');
                    if (index != -1) {
                        // index est la position avant [CR]LF, indexnl la position après [CR]LF
                        var indexnl = index;
                        if (text.charAt(indexnl) == '\r') indexnl++;
                        indexnl++;

                        if (line == -1) {
                            // à la dernière ligne, considérer index, parce
                            // qu'on veut se placer avant [CR]LF
                            pos += index;
                        } else {
                            // avant la dernière ligne, considérer indexnl, parce
                            // qu'on veut avoir un compte exact
                            pos += indexnl;
                        }
                        text = text.substr(indexnl);
                    } else {
                        pos += text.length;
                        break;
                    }
                }
                this.setSelectionRange(pos, pos);
            }
            $this.focus();
        });
    };
})(jQuery);
