Como editar conteúdos do html pelo browser

Via em vários aplicativos web de CMS a opção do usuário editar o conteúdo de uma página pelo próprio browser, ao invés de ter que entrar numa outra interface. Ficava me perguntando como isso era feito, qual o tipo de mágica. Quando vi que o (famoso) Sharepoint, o CMS da Microsoft faz isso, comecei a ficar desesperançoso, pensando que seria com alguma saída proprietária, algum componente para o IE.

Mas uma pesquisada no Google me fez deparar com um atributo desconhecido por mim até hoje (mesmo depois de mais de 5 anos trabalhando com interfaces para web), não reconhecido pela W3C mas suportado por vários browsers (testei no Firefox do Ubuntu, MacOS X e Windows Vista e XP), Safari, IE 6 e 7: contenteditable

Elementos do (x)html podem ser editáveis com a propriedade contenteditable="true"
Faça um teste em qualquer html seu, colocando assim:

<div contenteditable="true">texto editável</div>

Você verá quea área é cliçavel, e um cursor aparece quando você clica. E pode editar!

Como disse, não é válidado pelo W3C, mas todos os browsers usam… muito útil para um CMS ou qualquer outro onde o usuário possa editar textos!

Abaixo, segue um exemplo de como habilitar/desabilitar uma div para edição. A partir daí, muita coisa pode ser feita 🙂

<html>
<head>
<title>Exemplo de uso – contenteditable</title>
<script type="text/javascript">
    function toggleEditArea() {
        var field = document.getElementById("edit_area");
        if(!field.getAttribute("contenteditable") || field.getAttribute("contenteditable")=="false")
            field.setAttribute("contenteditable", "true");
        else
            field.setAttribute("contenteditable", "false");
    }
</script>
</head>
<body>
<div id="edit_area">sadsadsadsadas</div>
<a href="?edit" onclick="toggleEditArea(); return false;">habalitar/desabilitar</a>
</body>
</html>

Criando sua própria classe para requisições assíncronas – ajax

Sabe esses frameworks javascript que facilitam a nossa vida? Que tem funções, crossbrowsers, prontas para consultas ajax? Então, que tal ver como implementar isso?

E, não, não quero reinventar a roda! Serve mesmo para estudos ou no caso em que não é possível adotar um framework – sim, de vez em quando precisamos implementar algo só com funções nativas do javascript por requisitos do sistema…

Primeiro, criei uma função javascript chamada MyAjaxLoader(). Nela, instancio um objeto que fará as nossas consultas, de uma forma crossbrowser. Esse código, que verifica se existe o ActiveX (browsers IE) ou não, já foi muito explorado em diversos blogs. Indico essa leitura.
O próximo passo é criar uma função load() e adicioná-la a função MyAjadLoader() com a propriedade prototype (não confundir isso com a biblioteca homônima). Essa técnica permite criarmos funções com funções dentro, o que de uma certa forma pode ser abstraído para uma classe.
Essa função recebe dois parâmetros: a url que será requisitada e uma lista de chaves/valor de opções. Nessa lista de chaves/valor estou tratando apenas duas chaves: params, que podem ser os parâmetros a serem passados para a url e onSuccess, a função que será chamada em caso de sucesso na requisição (veja aqui texto sobre como passar funções de callback)

Abaixo, segue um exemplo funcional da classe!

<html>
<head>
<title>Criando sua própria classe para requisição assíncrona (ajax)</title>
<script type="text/javascript">

/**
* classe para requisições assíncronas – código nativo
*/
function MyAjaxLoader() {

    try {
        xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
    } catch (e) {
        try {
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        } catch (E) {
        xmlhttp = false;
        }
    }

    if  (!xmlhttp && typeof  XMLHttpRequest != "undefined" ) {
        try  {
        xmlhttp = new  XMLHttpRequest();
        } catch  (e) {
        xmlhttp = false ;
        }
    }
    this.xmlhttp = xmlhttp;

}

/**
* método que recebe a url a ser requisitada e pode receber parâmetros adicionais:
* params: parâmetros da url
* onSuccess: função de callback
*/
MyAjaxLoader.prototype.load = function(url, p) {
    //verifica se tem no parâmetro uma chave igual a params; se tiver, concatena com a url
    if(p.params)
        var myUrl = url + "?" + p.params;
    else
        var myUrl = url;
    //abre a conexão
    xmlhttp.open("get", myUrl);
    //faz a requisição
    xmlhttp.send(null);
    var thisMMP = this;
    //verifica os estados de transição
    xmlhttp.onreadystatechange = function() {
            //4 é o final da requisição
            if(xmlhttp.readyState==4) {
                //erro 404: não existe a url ou houve erro ao requisitar
                if(xmlhttp.status==404) {
                    alert("Erro: url não encontrada");
                    return;
                }
                //verifica se tem no parâmetro uma chave igual a onSuccess; se tiver, chama a
                //função de callback passando o resultado da requisição
                if(p.onSuccess)
                    p.onSuccess.call(this, xmlhttp.responseText);   
        }
    }
}

var myAjax = new MyAjaxLoader();
myAjax.load("tmp.htm", {params: "id=1&method=x", onSuccess: functionOk});

/* função de callback */
function functionOk(data) {
    alert(data);
}

</script>
</head>
<body>
</body>
</html>

Construindo uma classe PHP para acessar a API do BlogBlogs

Postei nesse último final-de-semana uma classe em PHP que fiz para acessar a API do BlogBlogs de uma forma fácil.
A API do BlogBlogs retorna através da arquitetura REST arquivos XML que representam informações do perfil de um usuário do BlogBlogs e de blogs cadastrados no sistema.

Download da classe aqui!

Primeiramente, criei duas classes, que abstraem esses dados em forma de objetos. Uma classe é a BlogBlogsBlog, um bean para a entidade blog, e outra a BlogBlogsUser, que por sua vez é um bean para o usuário do sistema. Cada classe recebe no construtor os atributos.

/**
* Dados do usuário do BlogBlogs
*/
class BlogBlogsUser {
    function __construct($username, $firstname, $lastname, $fullname, $thumbnailpicture, $profilepage, $blogs, $favorites, $fans) {
        $this->userName = $username;
        $this->firstName = $firstname;
        $this->lastName = $lastname;
        $this->fullName = $fullname;
        $this->thumbnailPicutre = $thumbnailpicture;
        $this->profilePage = $profilepage;
        $this->blogsCount = $blogs;
        $this->favoritesCount = $favorites;
        $this->fansCount = $fans;
        $this->blogs = array();
    }
   
}

/**
* Dados de um blog do BlogBlogs
*/
class BlogBlogsBlog {
    function __construct($name, $url, $rssurl, $blogpage, $lastupdate, $inboundblogs, $inboundlinks, $rank, $lang) {
        $this->name = $name;
        $this->url = $url;
        $this->rssUrl = $rssurl;
        $this->blogPage = $blogpage;
        $this->lastUpdate = $lastupdate;
        $this->inboundBlogs = $inboundblogs;
        $this->inboundLinks = $inboundlinks;
        $this->rank = $rank;
        $this->lang = $lang;
    }
}

Com isso, podia continuar a implementação. Criei a classe BlogBlogsService. Guardei no atributo apiUrl a url da API, e no construtor da classe recebo o valor da chave da API do BlogBlogs (semelhante ao processo de se usar a API do Google Maps, por exemplo).
Então criei alguns métodos, e os que são úteis para o desenvolvedor:

  • setUserName(): seta o username do usuário pelo qual faremos a busca no sistema do BlogBlogs. O método automaticamente busca todos os blogs e os dados do perfil do usuário
  • getBlogByUrl(): faz a busca por um blog através da sua url

Assim, temos implementadas todas as buscas possíveis pela API.

class BlogBlogsService {
    private $apiUrl = "http://api.blogblogs.com.br/api/rest/";
    private $userKey;
    private $doc;
   
    /**
    * construtor: recebe a chave da API do BlogBlogs
    * para gerar, acesse http://blogblogs.com.br/dev/chave
    */
    function __construct($k) {
            $this->userKey = $k;
            $this->user = null;
    }
   
    /**
    * recebe o userName no blogblogs e faz a consulta
    */
    function setUserName($userName) {
        $path = $this->apiUrl . "userinfo?key=" . $this->userKey . "&username=" . $userName;
        $this->doc = new DOMDocument();
        $this->doc->load($path);
        //verifica erros: caso tenha um nó com namespace error
        if($this->doc->getElementsByTagName("error")->length==1) {
            $this->errorMsg = $this->doc->getElementsByTagName("error")->item(0)->nodeValue;
            throw new Exception($this->errorMsg);
            return false;
        }
       
   
        if($this->doc->getElementsByTagName("result")->length!=1)
            return;
        //dados do usuário
        $userset = $this->doc->getElementsByTagName("result")->item(0);
        $blogBlogsUser = new BlogBlogsUser($userset->getElementsByTagName("username")->item(0)->nodeValue,
                                              $userset->getElementsByTagName("firstname")->item(0)->nodeValue,
                                              $userset->getElementsByTagName("lastname")->item(0)->nodeValue,
                                              $userset->getElementsByTagName("fullname")->item(0)->nodeValue,
                                           $userset->getElementsByTagName("thumbnailpicture")->item(0)->nodeValue,
                                              $userset->getElementsByTagName("profilepage")->item(0)->nodeValue,
                                           $userset->getElementsByTagName("blogs")->item(0)->nodeValue,
                                              $userset->getElementsByTagName("favorites")->item(0)->nodeValue,
                                           $userset->getElementsByTagName("fans")->item(0)->nodeValue);
        //dados de cada blog
        foreach($this->doc->getElementsByTagName("weblog") as $blog) {
            $blog = $this->createBlogWithXMLNode($blog);
            //$blogBlogsUser->addBlog($blog);
            $this->addBlogToUser($blogBlogsUser, $blog);
        }
        $this->user = $blogBlogsUser;
        return true;
    }
   
    /**
    * recupera um blog por uma url
    */
    function getBlogByUrl($url) {
        $path = $this->apiUrl . "bloginfo?key=" . $this->userKey . "&url=" . $url;
        $this->doc = new DOMDocument();
        $this->doc->load($path);
        if($this->doc->getElementsByTagName("weblog")->length!=1)
            return;
        $blog = $this->doc->getElementsByTagName("weblog")->item(0);
        $myBlog = $this->createBlogWithXMLNode($blog);
        return $myBlog;
   
    }
   
    function createBlogWithXMLNode($node) {
        $blog = new BlogBlogsBlog($node->getElementsByTagName("name")->item(0)->nodeValue,
                                  $node->getElementsByTagName("url")->item(0)->nodeValue,
                                  $node->getElementsByTagName("rssurl")->item(0)->nodeValue,
                                  $node->getElementsByTagName("blogpage")->item(0)->nodeValue,
                                  $node->getElementsByTagName("lastupdate")->item(0)->nodeValue,
                                  $node->getElementsByTagName("inboundblogs")->item(0)->nodeValue,
                                  $node->getElementsByTagName("inboundlinks")->item(0)->nodeValue,
                                  $node->getElementsByTagName("rank")->item(0)->nodeValue,
                                  $node->getElementsByTagName("lang")->item(0)->nodeValue);
   
        return $blog;
    }
   
    function addBlogToUser($user, $blog) {
        array_push($user->blogs, $blog);
    }
}

Vamos ver um passo-a-passo de como ver um atributo de um usuário:

$minha_chave = “XXXXXXXXXXXXXXX”;
$bbService = new BlogBlogsService($minha_chave);
$bbService->setUserName("ChrisB");
echo $bbService->user->fullName;

Se quisermos listar os blogs dele, exibindo as urls dos mesmos, acessamos a lista de BlogBlogsBlog que o BlogBlogsUser possue:

foreach($bbService->user->blogs as $blog) {
$blog->url . “<br />”;
}

Ainda podemos fazer a busca diretamente po um blog, pela sua url:

$bbService->getBlogByUrl(”http://www.chrisb.com.br/blog/”)
//para exibir o nome do blog
$bbService->getBlogByUrl(”http://www.chrisb.com.br/blog/”)->name;

Creio que os exemplos, mesmo simples, mostram a facilidade de uso. []s!

Usando funções de callback em javascript para sucesso ou erro

Em mais um post sobre funções como parâmetro de outras funções no javascript, vou mostrar como fazer para passar uma série de funções para determinados fins – como em caso de sucesso, em caso de falha, etc…

Onde sempre vemos isso? Frameworks como jQuery e Prototype dão a opção de você escolher uma função de callback para sucesso de um evento, outra para falha, outra para quando se inicia o drag de um elemento na tela, e por aí vai. Isso é feito passando, ao invés de uma função, uma lista de funções. No caso, para poder dar nome a elas, usamos a estrutura de chave/valor (key/value).

O exemplo abaixo explica como a função wrapper() passa 2 parâmetros e duas funções de callback: uma para sucesso, outra para falha. Na primeira chamada, a função de callback chamada será a de sucesso, pois é possível efetuar a rotina. No outro caso, dará erro, então será usada a função de falha.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Untitled Document</title>
<script type="text/javascript">
function wrapper(par1, par2) {
    //verifica se o último parâmetro é uma função para chamá-la;
    //os demais parámetros são repassados para a função

    var lastArgument = arguments[arguments.length-1];
   
    try {
        //Classe Math possue o método max(), que retorna o maior valor entre dois parâmetros
        var sum = Math.max(par1, par2);
        //se não for erro, joga (throw) erro
        if(isNaN(sum))
            throw "not a valid result";
        //se chegar até aqui, é um resultado válido e chama função de sucesso
        lastArgument.onSuccess.call(this, sum);
    } catch(e) {
        //se não, chama a função de erro
        lastArgument.onFailure.call(this, e);
    }
       
   
}

function errorHandler(e) {
    alert("Error: " + e);
}

function myFunction(sum) {
    alert(sum);
}
wrapper(10, 2, {onSuccess: myFunction, onFailure:errorHandler}); //será chamado sucesso
wrapper(10, "a", {onSuccess: myFunction, onFailure:errorHandler}); //será chamado erro
</script>
</head>
<body>

</body>
</html>

 

Passando uma função e outros parâmetros para outra função em javascript

Seguindo o que comecei a explicar nesse post sobre como passar funções como parâmetro para outra no javascript, vou mostrar agora rapidamente como passar outros parâmetros – texto, string, o que você precisar.

Esse exemplo abaixo funciona para o caso de você saber exatamente quantos parâmetros serão passados para a função e quantos recebidos pela função de callback.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Untitled Document</title>
<script type="text/javascript">
function wrapper(par1, par2) {
    //verifica se o último parâmetro é uma função para chamá-la;
    //os demais parámetros são repassados para a função  
    if(arguments.length>0) {
        var lastArgument = arguments[arguments.length-1];
        if(typeof(lastArgument)=="function") {
            lastArgument.call(this, par1, par2);   
        }
    }
}
function myFunction(par1, par2) {
    alert(par1);
    alert(par2);
}
wrapper("a", "b", myFunction);
</script>
</head>
<body>
</body>
</html>

O exemplo funciona da seguinte forma: a função wrapper recebe duas strings e uma função. Nela, verifica-se se o último parâmetro é uma funcão e, se for, ela chama essa função passando essas duas strings como parâmetro.
Note que esse exemplo vale para a função sendo passada como último parâmetro. Ela pode ser passada em outra ordem, mas aí deve ser feita a devida checagem.

Nos próximos post, mostro como receber uma quantidade aleatória de parâmetros e tratar de repassá-los (usando JSON) e como definir funções de callback específicas dependendo de condições na wrapper.