Gerenciando suas tarefas com HTML5, IndexedDB, CSS3 e jQuery

Recentemente conheci o Wunderlist, uma aplicação para gerenciamento de tarefas que possui aplicações para desktop, Android, iOS, Web e, se duvidar, até para o seu George Foreman Grill. Gostei bastante da ferramenta e já estou usando em praticamente todos os meus devices, com uma sincronia perfeita. Gostei tanto que resolvi utilizar de exemplo nesse post para criar um gerenciador de tarefas simples com armazenamento local, utilizando IndexedDB.

Com essa pequena aplicação será composta de apenas 5 arquivos você poderá fazer a criação e a exclusão de tarefas, que podem ser organizadas por categorias. Neste primeiro post, a intenção é mostrar as funcionalidades da API IndexedDB que temos agora no HTML5.

Bom, chega de enrolar e vamos ao que interessa.

Começamos com um arquivo HTML bem simples.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Gerenciando Tarefas - HTML5 - IndexedDB</title>
    <link rel='stylesheet' href='css/style.css' />
    <script src="js/jquery-1.8.3.min.js"></script>
    <script src="js/scripts.js"></script>
</head>
<body>
    <section id="geral">
        <aside id='barra_menus'>
            <header id="topo">
                <h1 id="titulo">Tarefas</h1>
            </header>
            <nav>
                <ul class="listaCategorias">

                </ul>
            </nav>
        </aside>
        <article id="conteudo">
            <span class="formulario">
                <input type="hidden" name="categoria" id="categoria" value="">
                <input type="text" name="tarefa" id="tarefa" placeholder="Adicionar nova tarefa">
            </span>
            <br/>
            <ul class="listaTarefas">

            </ul>            
        </article>
    </section>  
</body>
</html>

Viu como é simples mesmo ? Agora o CSS para deixar a nossa aplicação com uma ‘carinha bonita’.

@charset "UTF-8";

* {margin: 0; padding: 0;}
a {text-decoration : none;}

/*
    Estilo geral
*/
body {
    background: #fff url('../img/wood.jpg') no-repeat top center;
    color : #000;
    font-family : Helvetica,sans-serif;
    font-size : 10pt;
    overflow: hidden;
    text-align : left;
}

html, body {
    height: 100%;
}

#geral {
    color : #000;
    min-height : 100%;
    margin : auto;
    position : relative;
}

#topo {
    background: #555; 
    box-shadow: 0px 0px 5px 1px rgba(0,0,0,.5);
    height : 40px;
    padding : 0 0 0 0;   
    text-align: center;
    z-index: 999;
}

#titulo {
    color : #eaeaea;
    font-size: 15pt;
    font-weight : bolder;
    padding : 3px;
    line-height: 40px;
    text-shadow: 0px 0px 6px rgba(255,255,255,.7);
    top : 5px;
}

#barra_menus {
    background: #eaeaea;
    box-shadow: 5px 0px 5px rgba(0,0,0,.5);
    height: 100%;
    width:270px;
    position: fixed;
}

nav li {
    border-bottom: 1px dotted #ccc;
    color: #555;
    cursor: pointer;
    display: block;
    font-weight: bold;
    padding: 10px;
}

nav li.ativo {
    background: #4876FF;
    border: 1px inset;
    color: #fff;
}

nav li a {
    color: #555;
    float: right;
}

#conteudo {
    color : #000;
    padding : 20px 0px 0px 270px;
}

/* 
    Formulário de tarefa;
*/
.formulario {
    padding: 20px;
}
#tarefa {
    background: rgba(0,0,0,.3);
    border: 1px inset #ccc;
    border-radius: 5px;
    box-shadow: 2px 2px 5px rgba(0,0,0,.5) inset;
    color: #fff;
    margin:3px;
    padding: 10px 20px;
    text-shadow: 2px 2px 2px rgb(0,0,0);
    width: 90%;
}

/* 
    Listagem de tarefas;
*/
.listaTarefas {
    list-style: none;
    overflow: auto;
    max-height: 500px;
    padding: 20px;
}

.listaTarefas li {
    background: #eaeaea;
    border: 1px outset rgba(255,255,255,.9);
    border-radius: 5px;
    color: #555;
    margin: 2px;
    padding: 10px;
}

.listaTarefas li a {
    float: right;
}

Se você está usando um navegador decente, a aplicação ficará igual à imagem no início do post. Caso contrário, você terá que fazer alguns ajustes no CSS. Testei o layout no Chrome, Firefox (última versão estável) e IE9 e não tive problemas de renderização.

Agora vamos mágica.

$(document).ready(function(){

    var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
    var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
    var db;

    if (!window.indexedDB) {
        alert("Seu navegavor não suporta a API IndexedDB.");
    } else {
        bdTarefas();        
    }   

    function bdTarefas(){
        var request = indexedDB.open("Tarefas", 1);  
        request.onsuccess = function (evt) {
            db = request.result;
            $('#categoria').val('Entrada');
            var categoria = $('#categoria').val();
            $('#tarefa').attr('placeholder', "Adicionar um item em \"" + categoria + "\"");        
            listaTarefas();
            listaCategorias();
        };

        request.onerror = function (evt) {
            console.log("IndexedDB error: " + evt.target.errorCode);
        };

        request.onupgradeneeded = function (evt) {
            var storeTarefas = evt.currentTarget.result.createObjectStore("tarefas", { keyPath: "id", autoIncrement: true });
            storeTarefas.createIndex("tarefa", "tarefa", { unique: false });
            storeTarefas.createIndex("categoria", "categoria", { unique: false });
            var storeCategorias = evt.currentTarget.result.createObjectStore("categorias", { keyPath: "id", autoIncrement: true });
            storeCategorias.createIndex("categoria", "categoria", { unique: true });
        };
    }

    function listaTarefas() {
        var tarefas = $('.listaTarefas');
        tarefas.empty();        
        var transaction = db.transaction("tarefas", "readwrite");
        var objectStore = transaction.objectStore("tarefas");

        var req = objectStore.openCursor();
        req.onsuccess = function(evt) {  
            var cursor = evt.target.result;  
            if (cursor) {
                if(cursor.value.categoria == $('#categoria').val()) {
                    var linha  = "<li>" + cursor.value.tarefa + "<a href='#' id='" + cursor.key + "'>[ Delete ]</a></li>";
                    $('.listaTarefas').append(linha);
                }
                cursor.continue();  
            }  
        };          
    }

    function listaCategorias() {
        var tarefas = $('.listaCategorias');
        tarefas.empty();        
        $('.listaCategorias').append("<li id='Entrada' class='categorias ativo'>Entrada</li>");
        var transaction = db.transaction("categorias", "readwrite");
        var objectStore = transaction.objectStore("categorias");

        var req = objectStore.openCursor();
        req.onsuccess = function(evt) {  
            var cursor = evt.target.result;  
            if (cursor) {
                var linha  = "<li id='"+ cursor.value.categoria +"' class='categorias'>" + cursor.value.categoria + "<a href='" + cursor.value.categoria + "' id='"+cursor.key+"'>[ X ]</a></li>";
                $('.listaCategorias').append(linha);
                cursor.continue();  
            } else {
                $('.listaCategorias').append('<li id="nova">+ Adicionar Nova Categoria</li>');                                
            }
        };   
    }    

    function insereTarefa() {
        var categoria = $('#categoria').val();
        var tarefa = $("#tarefa").val();

        var transaction = db.transaction("tarefas", "readwrite");
        var objectStore = transaction.objectStore("tarefas");                    
        var request = objectStore.add({tarefa: tarefa, categoria: categoria});
        request.onsuccess = function (evt) {
            $('#tarefa').val('');
            listaTarefas(); 
        };                   
    }    

    function insereCategoria() {
        var categoria = $('#nova_categoria').val();

        var transaction = db.transaction("categorias", "readwrite");
        var objectStore = transaction.objectStore("categorias");                    
        var request = objectStore.add({categoria: categoria});
        request.onsuccess = function (evt) {
            $('#nova_categoria').val('');
            listaCategorias(); 
        };                   
    }        

    function deletaTarefa(id) {
        var transaction = db.transaction("tarefas", "readwrite");
        var store = transaction.objectStore("tarefas");
        var req = store.delete(+id);
        req.onsuccess = function(evt) {  
            listaTarefas();
        };
    }

    function deletaCategoria(id, categoria) {
        var transaction = db.transaction("categorias", "readwrite");
        var store = transaction.objectStore("categorias");
        var req = store.delete(+id);
        req.onsuccess = function(evt) {
            limpaTarefasSemCategoria(categoria);
            bdTarefas();
        };
    }

    function limpaTarefasSemCategoria(categoria) {
        var transaction = db.transaction("tarefas", "readwrite");
        var objectStore = transaction.objectStore("tarefas");

        var req = objectStore.openCursor();
        req.onsuccess = function(evt) {  
            var cursor = evt.target.result;  
            if (cursor) {
                if(cursor.value.categoria == categoria) {
                    var del = objectStore.delete(cursor.key);
                }
                cursor.continue();  
            }
        };           
    }

    $('#tarefa').keypress(function (e) {
        if(e.keyCode == 13) {
            insereTarefa();
        }
    });

    $('.listaTarefas').on('click', 'a', function(){
        var confirma = confirm('Deseja excluir esta tarefa ?')
        if(confirma) {
            deletaTarefa(this.id);
            return false;
        }
    });

    $('.listaCategorias').on('click', 'a', function(){
        var confirma = confirm('Deseja excluir esta categoria ?\nATENÇÃO: Esta ação excluirá todas as tarefas vinculadas a esta categoria!')
        if(confirma){
            var id = this.id;
            var categoria = $(this).parent().get(0).id;
            deletaCategoria(id, categoria);       
        }
        return false;        
    });    

    $('.listaCategorias').on('keypress', '#nova_categoria', function(e) {
        if(e.keyCode == 13) {
            insereCategoria();
        }
    });

    $('.listaCategorias').on('click', '.categorias', function(){
        $('nav li').removeClass('ativo');
        $(this).addClass('ativo');        
        $('#categoria').val('');
        var id = this.id;
        $('#categoria').val(id);
        $('#tarefa').attr('placeholder', "Adicionar um item em \"" + id + "\"");        
        listaTarefas();
    });

    $('.listaCategorias').on('click','#nova', function(){
        $(this).replaceWith('<li id="nova"><input type="text" name="nova_categoria" id="nova_categoria" placeholder="Adicionar nova categoria"></li>')
        $('#nova_categoria').focus();
    });

    $('.listaCategorias').on('blur', '#nova_categoria', function(){
        $(this).replaceWith('+ Adicionar Nova Categoria')
    });
});

No código abaixo criamos as variáveis que armazenam as informações da API IndexedDB de acordo com o navegador utilizado e fazemos uma pequena verificação de compatibilidade;

    var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
    var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
    var db;

    if (!window.indexedDB) {
        alert("Seu navegavor não suporta a API IndexedDB.");
    } else {
        bdTarefas();        
    }

Caso o navegador seja compatível com a tecnologia é chamada a função bdTarefas() que é responsável por abrir a conexão com o nosso banco.

    function bdTarefas(){
        var request = indexedDB.open("Tarefas", 1);  
        request.onsuccess = function (evt) {
            db = request.result;
            $('#categoria').val('Entrada');
            var categoria = $('#categoria').val();
            $('#tarefa').attr('placeholder', "Adicionar um item em \"" + categoria + "\"");        
            listaTarefas();
            listaCategorias();
        };

        request.onerror = function (evt) {
            console.log("IndexedDB error: " + evt.target.errorCode);
        };

        request.onupgradeneeded = function (evt) {
            var storeTarefas = evt.currentTarget.result.createObjectStore("tarefas", { keyPath: "id", autoIncrement: true });
            storeTarefas.createIndex("tarefa", "tarefa", { unique: false });
            storeTarefas.createIndex("categoria", "categoria", { unique: false });
            var storeCategorias = evt.currentTarget.result.createObjectStore("categorias", { keyPath: "id", autoIncrement: true });
            storeCategorias.createIndex("categoria", "categoria", { unique: true });
        };
    }

Nas linhas 4 à 9, temos algumas funções executadas caso a requisição de abertura banco seja concluída com sucesso. Na linha 13, enviamos para o console um aviso caso a requisição execute com erro e no final (linhas 17 à 21) fazemos a criação das nossas tabelas e colunas.

As funções abaixo (listaTarefas() e listaCategorias()) são responsáveis por listar as informações de cada uma das tabelas da nossa aplicação. A função listaTarefas() começa limpando o elemento que possui a classe .listaTarefas . Em seguida fazemos a conexão com o o objeto tarefas e incluímos os resultados dentro da classe .listaTarefas, apenas das tarefas relacionadas à categoria selecionada.

A função listaCategorias() inclui no nosso menu lateral uma categoria padrão, lista as categorias criadas pelo usuário, e insere um link no final para o usuário poder criar novas categorias.

    function listaTarefas() {
        var tarefas = $('.listaTarefas');
        tarefas.empty();        
        var transaction = db.transaction("tarefas", "readwrite");
        var objectStore = transaction.objectStore("tarefas");

        var req = objectStore.openCursor();
        req.onsuccess = function(evt) {  
            var cursor = evt.target.result;  
            if (cursor) {
                if(cursor.value.categoria == $('#categoria').val()) {
                    var linha  = "<li>" + cursor.value.tarefa + "<a href='#' id='" + cursor.key + "'>[ Delete ]</a></li>";
                    $('.listaTarefas').append(linha);
                }
                cursor.continue();  
            }  
        };          
    }

    function listaCategorias() {
        var tarefas = $('.listaCategorias');
        tarefas.empty();        
        $('.listaCategorias').append("<li id='Entrada' class='categorias ativo'>Entrada</li>");
        var transaction = db.transaction("categorias", "readwrite");
        var objectStore = transaction.objectStore("categorias");

        var req = objectStore.openCursor();
        req.onsuccess = function(evt) {  
            var cursor = evt.target.result;  
            if (cursor) {
                var linha  = "<li id='"+ cursor.value.categoria +"' class='categorias'>" + cursor.value.categoria + "<a href='" + cursor.value.categoria + "' id='"+cursor.key+"'>[ X ]</a></li>";
                $('.listaCategorias').append(linha);
                cursor.continue();  
            } else {
                $('.listaCategorias').append('<li id="nova">+ Adicionar Nova Categoria</li>');                                
            }
        };   
    }

As funções abaixo executam a tarefa de incluir os dados nas tabelas.

    function insereTarefa() {
        var categoria = $('#categoria').val();
        var tarefa = $("#tarefa").val();

        var transaction = db.transaction("tarefas", "readwrite");
        var objectStore = transaction.objectStore("tarefas");                    
        var request = objectStore.add({tarefa: tarefa, categoria: categoria});
        request.onsuccess = function (evt) {
            $('#tarefa').val('');
            listaTarefas(); 
        };                   
    }    

    function insereCategoria() {
        var categoria = $('#nova_categoria').val();

        var transaction = db.transaction("categorias", "readwrite");
        var objectStore = transaction.objectStore("categorias");                    
        var request = objectStore.add({categoria: categoria});
        request.onsuccess = function (evt) {
            $('#nova_categoria').val('');
            listaCategorias(); 
        };                   
    }

Na sequência temos as funções de deleção de registros. A função deletaCategoria(id,categoria) possui uma função complementar, para excluir todas as tarefas relacionadas àquela categoria para que não fiquem registros perdidos.

    function deletaTarefa(id) {
        var transaction = db.transaction("tarefas", "readwrite");
        var store = transaction.objectStore("tarefas");
        var req = store.delete(+id);
        req.onsuccess = function(evt) {  
            listaTarefas();
        };
    }

    function deletaCategoria(id, categoria) {
        var transaction = db.transaction("categorias", "readwrite");
        var store = transaction.objectStore("categorias");
        var req = store.delete(+id);
        req.onsuccess = function(evt) {
            limpaTarefasSemCategoria(categoria);
            bdTarefas();
        };
    }

    function limpaTarefasSemCategoria(categoria) {
        var transaction = db.transaction("tarefas", "readwrite");
        var objectStore = transaction.objectStore("tarefas");

        var req = objectStore.openCursor();
        req.onsuccess = function(evt) {  
            var cursor = evt.target.result;  
            if (cursor) {
                if(cursor.value.categoria == categoria) {
                    var del = objectStore.delete(cursor.key);
                }
                cursor.continue();  
            }
        };           
    }

O restante do javascript contém algumas funções de perfumaria da aplicação. Alteração de classe da categoria selecionada, inclusão de funcionalidade de deleção nos links, etc.

Fiz este post com o intuito de demonstrar as funcionalidades da API IndexedDB. Para maiores informações, você pode consultar os links fonte no final do post.

Veja a demonstração da aplicação e baixe os fontes do meu Github. Faça seus próprios testes, melhore o código, inclua novas funcionalidades, questione, critique. Em um próximo post, vou mostrar como sincronizar essas informações com um banco de dados relacional em um servidor web.

Leave a Reply