Friday, May 22, 2015

Making Bitcoin Paywall Middleware

Today's comic doesn't include a lot of built-in technology like Wednesday's did. Just a moth joke. Or was that just a mistake? I'm not sure. Moving on...

Wednesday's comic had a Bitcoin paywall, and in Wednesday's blog I explained how I processed a 402 response in the client to make it work. But the server played more than a bit part in the whole operation. Get it? Bit part. Bitcoin. Moving on...

It's like regular money, but with more math.
To handle the Bitcoin payments and unlock the content, there are several pieces at work. I am using the Express framework for my web application, so I have a middleware applied to each non-free resource. There is also a Bitcoin address generator to create new payment addresses. Another resource serves as the payment notification - when payment is made to an address, this resource receives data about the transaction. Finally, there is a datastore that manages the records to keep all the other pieces in sync.

This is how it works:

First, a resource is requested which requires payment. The middleware checks with the datastore to see if the requesting client has an existing record for this resource.
If no existing record is found, it means the client has never attempted to access this resource before. The middleware uses the address generator to create a new payment address record and gives it to the datastore. The client is returned the 402 response along with the special payment instruction headers.
Alternatively, the datastore might find a record for this resource and client. That means the client has been here before. The record returned by the datastore will indicate whether or not the payment is complete. If the payment has been made, the middleware allows normal access. If payment has not been made, the middleware returns the 402 response to the client with the special payment instruction headers. The data for those headers comes out of the payment record.
At any time, an external Bitcoin processing system can determine that payment has been made to one of the addresses generated by the address generator. It sends data on the payment transaction to the payment callback resource which looks up the record in the datastore and marks it as paid.

I used the Coinbase Merchant API as the external Bitcoin processing system. When I create addresses with Coinbase, I can specify a callback URL for each one and Coinbase will POST some data to it when payment is made. Each address gets a unique URL so my system can look up the payment record that matches it. I am using the official Coinbase Node module in my app.

Here is a basic outline of the app:

var express = require("express");
var app = express();

// mock purchase record
var demoRecord = {
    recordId: "12345",
    code: "product-code",
    paid: false,
    cost: 123.45,
    address: "bitcoinaddress",
    secret: "itsasecret"
};

// mock datastore object
var datastore = {
    findRecord: function(req, productCode, callback) {
        callback("12345");
    },
    checkPaidStatus: function(recordId, callback) {
        callback(demoRecord);
    },
    newRecord: function(data) {
        // store the record
    },
    findRecordBySecret: function(secret, callback) {
        if (secret === demoRecord.secret) {
            callback(demoRecord);
        } else {
            callback(null);
        }
    }
};

function newBitcoinAddress(productCode, callback) {
    callback(null, demoRecord);
}

function send402(res, data) {

    res.setHeader("X-Payment-Types-Accepted", "Bitcoin");
    res.setHeader("X-Payment-Address-Bitcoin", data.address);
    res.setHeader("X-Payment-Amount-Bitcoin", data.cost);
    res.sendStatus(402);

}

function paywallMiddleware(req, res, next) {

    // let's use the URL as the product code,
    // since that's really what is being sold
    var productCode = req.path;

    datastore.findRecord(req, productCode, function(recordId) {

        if (recordId) {

            // found a record, which means that this client
            // has a payment address and purchase record already.
            // now check to see if it has been paid.
            datastore.checkPaidStatus(recordId, function(data) {

                if (data.paid) {

                    next(); // all paid, move along...

                } else {

                    // respond with the payment address that already exists
                    send402(res, data);

                }

            });

        } else {

            // no record found, which means a new Bitcoin
            // address and purchase record must be created.

            newBitcoinAddress(productCode, function(err, data) {

                if (err) {
                    next(err);
                } else {

                    datastore.newRecord(data);
                    send402(res, data);

                }

            });

        }

    });

}

app.get("/free", function(req, res, next) {
    res.send("free content");
});

app.get("/paid", paywallMiddleware, function(req, res, next) {
    res.send("paid content");
});

app.get("/callback", function(req, res, next) {

    var secretCode = req.query.secret;
    datastore.findRecordBySecret(secretCode, function(record) {
        if (record) {
            record.paid = true;
        }
    });
    res.sendStatus(200);

});

// ------------ start listening
var server = app.listen(3000, function() {
    console.log("listening on port %d", server.address().port);
});

That looks like a lot of code, but it's not really that complicated. Near the top, you see I have some mock objects for the demo. In a real scenario, your datastore object would access an RDBMS or the file system to store and retrieve actual purchase records. Here, though, a single fake record and some stub functions will suffice. The newBitcoinAddress function is also a stub - in the real app, this is where I use the Coinbase API to create addresses and associated purchase records each with their own unique secret code as part of the callback URL. That secret code should never be returned to the user, but needs to be stored so the record can be retrieved later in the callback handler. The send402 function is just a utility function that sets the special response headers and sends the 402 code.

The paywallMiddleware function is the implementation of the algorithm I described above. It uses the mock datastore and address generation function.

The routes at the bottom allow testing the system. Launch the app and point your browser to http://localhost:3000/free. You should get the free content. Now try http://localhost:3000/paid. You should get a 402 response. This is because the /paid route includes the paywallMiddleware. To get access, you'll need to pay.

This demo doesn't require any real Bitcoin transactions to take place. To simulate the callback coming from Coinbase, I included a /callback route that is a GET, which makes it easy to test from your browser. In real life, this will be a POST. To simulate sending payment to the address, you just have to go to http://localhost:3000/callback?secret=itsasecret. When you do, you should get a simple OK response...but on the server side, the demo record was updated to paid because the secret code matched. Now you can go back to http://localhost:3000/paid and get the paid content. If you had used a different (incorrect) value for secret, the /paid resource would not have been unlocked.

Next week I'll go into a little more detail about how I use the Coinbase API. My hope is that someday more content creators such as myself can use functions like this for micropayments in place of running advertisements on their sites.

But that's just a dream right now. How long will it take to become reality? A few moths? I mean, months?

Amphibian.com comic for 22 May 2015

Wednesday, May 20, 2015

Using HTTP 402 for a Bitcoin Paywall

Bitcoin. Perfect for Microtransactions?
The punchline for today's comic is hidden behind a paywall. It is a literal wall, built by the frogs to hide the last frame of the comic. However, if you have some Bitcoin to spare (it costs 0.001 coins, about $0.25 US at the present time) you can actually pay for the wall to be removed.

A fair amount of interesting work went in to this comic, but it all revolves around the concept of using the HTTP 402 response code.

Response code 402 is officially "reserved for future use" but I felt that it was about time the future showed up. It's 2015. Where is my moon colony?? Anyway, 402 means "Payment Required" and was apparently intended to be a way for web servers to indicate to clients that the requested resource had to be purchased. Unfortunately, a way to pay for those resources was never worked out and the response code has languished in the realm of TBD for many years.

But these days we have Ajax, REST web services, and Bitcoin. It's all coming together. People have kicked around the notion of integrating 402's with Bitcoin transactions for a few years now but no significant implementation has emerged. With today's comic, sadly, nothing has changed.

I did, however, create a functional web content paywall for Bitcoin microtransactions. Here's how it works...

When a client - be it a human-operated web browser or another computer program - accesses a URL which does not dispense free content, the server will return a 402 response instead of the content. That response also includes three special headers, examples of which are shown here:

X-Payment-Types-Accepted: Bitcoin
X-Payment-Address-Bitcoin: putarealbitcoinaddresshere
X-Payment-Amount-Bitcoin: 1.234

The first, X-Payment-Types-Accepted, should be a comma-separated list of acceptable payment types. In my example and in my comic I am only accepting Bitcoin for now, although a similar form of payment such as Litecoin would easily be possible in place of or in addition to Bitcoin. Each item listed in this header should be used to check the other 2 headers. The X-Payment-Address-XXX header specifies an address to which the payment should be sent. The X-Payment-Amount-XXX header specifies how much this particular resource costs.

The key is that the last part of the address and amount headers should match an item in the list of types accepted. If I wanted to accept either Bitcoin or Litecoin, my headers might look like this:

X-Payment-Types-Accepted: Bitcoin, Litecoin
X-Payment-Address-Bitcoin: putarealbitcoinaddresshere
X-Payment-Amount-Bitcoin: 1.234
X-Payment-Address-Litecoin: putareallitecoinaddresshere
X-Payment-Amount-Litecoin: 2.345

It's then up to the client to deal with this response. Today, browsers will silently ignore this extra information and just tell you that the response code means "Payment Required" without letting you know how to pay. In the future, browsers might prompt you to submit the payment (see this: Zero Click Bitcoin Micropayments). Today, though, you have to do things manually.

In certain cases, the server could include the Bitcoin address and price in the HTML that accompanies the 402 response. Since I am just requesting the paid resources via Ajax, I didn't bother with that and don't include anything human-readable in the response. Here is how my client-side code works when you view the comic:

function checkForPayment() {

    $.ajax({
        url: "/paidContent/paywall-comic",
        dataType: "html",
        success: function(data) {

            $('#comicArea').html(data);

        },
        error: function(xhr, respText, et) {

            if (xhr.status === 402) {

                var addr = xhr.getResponseHeader("X-Payment-Address-Bitcoin");
                var cost = xhr.getResponseHeader("X-Payment-Amount-Bitcoin");

                var bcurl = "bitcoin:" + addr + "?amount=" + cost;
                var bcqr = encodeURIComponent(bcurl);

                if ($("#paydiv").html() === "") {
                    $("#paydiv").html("<p>Pay with Bitcoin!<br>" +
                                      "<a href='" + bcurl + "'><img src='/qrc?text=" + 
                                      bcqr + "'></a><br>" +
                                      "Send " + cost + " BTC to<br>" +
                                      addr + "</p>");
                }

                setTimeout(checkForPayment, 2000);

            } else {
                console.log("failed");
            }

        }

    });

}

When the free version (missing the last frame) of the comic loads, checkForPayment() is called and attempts to get the data for the complete comic from the URL /paidContent/paywall-comic. Anything on my site with a path starting with /paidContent/ will require purchasing. On a successful response, which you'll only get if you've paid, the unpaid version of the comic is replaced with the data from the server.

The interesting part is the error response handling on line 11. A 402 response is an error because it is in the 400 range - indicating a client-side error. It's not a very serious error - just asking for something for which you have not paid! I am using jQuery so the first parameter passed to the error handler function is the jqXHR object. There are many things you can do with that object, but I only need to check the response status to see if it is a 402, and if so I read the values of the X-Payment-Address-Bitcoin and X-Payment-Amount-Bitcoin headers. Yes, I am ignoring my own X-Payment-Types-Accepted header because I happen to know that it only contains Bitcoin (that's the only kind of coin I have right now). If I expected other types, the right thing to do would be to read the X-Payment-Types-Accepted header and loop over the list of values to get the names of the other headers.

I take the address and price and submit them to my QR Code image generation URL to make an easy, scannable way to pay - but also just display the address and price to the user. Again, I'm doing this on the client side because I'm requesting the data with Ajax. If the user navigated directly to a URL which required payment, the server could have responded with that HTML as part of the 402 page, much in the same way servers respond with custom (and sometimes helpful) 404 pages today.

The last part is to check the /paidContent/paywall-comic URL again to see if a payment has gone through. Since it can be a few seconds before Coinbase tells my server that payment has been sent to the address, I took the quick and dirty route of setting a timeout and running the whole checkForPayment() function again. I could have connected a Websocket to wait for payment confirmation from the server if I wanted to get fancy - a website that was expected to get more traffic than my comic would probably want to go that route instead. A possible future enhancement would be to include an additional header with a recommendation for the client - specifying if it should refresh, wait, redirect so some other URL, etc.

In the future, I might put more special features on Amphibian.com on the /paidContent/ URL, instead of it just being part of the joke. Maybe special comics that you can only access by paying a few cents? I'm not sure yet, but I tried to develop a framework that robust enough to support future expansion. At this point, I can easily make the server request payment for anything thanks to the Express middleware that I wrote combined with the Coinbase Node module. But that's too much for a single blog post so I'll talk about that on Friday!

I also hope that more web sites adopt and expand on this model now that I have it working in a semi-legitimate application. In my opinion, it beats displaying ads for enabling content creators to monetize. You can help me monetize by paying to get the whole comic today!

Amphibian.com comic for 20 May 2015

Monday, May 18, 2015

Enabling Sound Effects on Web Pages

Got something to say?
I threw in a simple effect in today's comic where the computer is playing an acoustic guitar. Push the play, pause, and stop buttons above it, and you can control some real acoustic guitar music. What used to be a monumental task - putting sound effects in a web page - is now extremely simple.

It occurred to me that while I've mentioned using SoundManager2 several times in the blog, I haven't actually shown an example of its use since 2010! Things have changed since then, so it's worth bringing up again.

I like SoundManager2 because it's easy. I've been using it since before there was an HTML5 Audio specification, and have been very thankful for it. Abstraction solves all problems in computer science, and SoundManager2 provides a great abstraction on top of all the different ways to play sounds across different browsers on different types of devices. Sometimes it will use HTML5, sometimes it will revert to Flash (hopefully not often these days). But it just works.

When I made my game entry for the GitHub Game Off 2015, I used the Crafty JavaScript game framework. To play sounds across different platforms, I had to provide the sound effects in MP3, OGG, and WAV formats. It chose the correct one to play for whatever the client's capability happened to be. But back in 2013, my Game Off entry was coded from scratch and I used SoundManager2. I only had to provide sounds in MP3 format, which made my life easier.

Not that it's too difficult to convert sound formats with Audacity, but it was an extra step that I would have preferred to avoid. But I digress...

I've put sound effects in more than one of my comics recently and the pattern is always the same. First, simply include SoundManager's JavaScript file in your page. It comes with both debug and non-debug versions of the full-size and minimized versions. For production, I use soundmanager2-nodebug-jsmin.js.

<script src="soundmanager2-nodebug-jsmin.js"></script>

After that, playing sounds is extremely simple. Here is the complete guitar.js file from today's comic:

var guitarSound = null;

$(function() {

    soundManager.setup({
        url : '/swf/',
        onready : function() {
            guitarSound = soundManager.createSound({
                id : 'guitarSound',
                url : '/audio/guitar.mp3'
            });
        },
        ontimeout : function() {
            console.log("could not start soundmanager!");
        }
    });

    $('#play-button').click(function() {
        if (guitarSound) {
            if (guitarSound.playState === 0 || guitarSound.paused) {
                guitarSound.play();
            }
        }
    });

    $('#pause-button').click(function() {
        if (guitarSound) {
            guitarSound.togglePause();
        }
    });

    $('#stop-button').click(function() {
        if (guitarSound) {
            guitarSound.stop();
        }
    });

});

Since I also use jQuery, I do all the setup inside an anonymous function passed in to $(). This simply ensures that the code inside won't be called until the page is completely loaded and ready.

The call to soundmanager.setup on line 5 passes in an object describing the configuration of SoundManager. The url field is very important as it specifies where SoundManager can find the Flash files it needs if fallback to Flash becomes necessary. Those files are packaged in the SoundManager download file inside the /swf directory so I mimicked that path structure on my web site. The onready field is a function that will be called when SoundManager has completely initialized itself. This is where it is safe to create sounds which can be played later. In my case, I create one sound, guitarSound, from a file in my web application's /audio directlory named guitar.mp3. The ontimeout function, on line 13, is what gets called in the error condition that SoundManager could not start. It might happen if you try to use it on Netscape Navigator 4, or you just messed up your configuration.

The rest of the code just adds click listeners to the three types of buttons. I always check to see if my sound exists before doing anything with it, just in case SoundManager failed to start. The pause and stop functions are simple - just calling togglePause() and stop(). Toggle pause is different from the alternate pause() function in that it will also un-pause the sound if called when paused. The stop function should be self-explanatory. The handling of play is a little more complicated because SoundManager (on most modern platforms) allows you to play a sound multiple times and it will mix with itself. If you don't want that, check the playState before calling play() to ensure that it's not already playing. I also allow play() to be called if the sound is paused, which has the same effect as un-pausing.

It really is that easy. Sound can add an important dimension to human/computer interaction, and thanks to SoundManager2 it need not be excluded from the web-based user experience. Just don't make every button click play an annoying sound. Please.

Amphibian.com comic for 18 May 2015

Friday, May 15, 2015

Is Remote Work Remotely Possible?

I wrote today's comic because I was thinking about working remotely. It seems that many types of modern software developer jobs can be done by people sitting in their homes instead of in a cubicle. There are probably many other types of jobs that could be done remotely as well. Why does it still seem like the exception instead of the rule?

Is it distrust of workers that you can't watch?

Is it a desire to have a cohesive company culture?

Is it an honest belief that workers need an office environment to be productive?

Is it just tradition?

As usual, I don't have any answers. Just questions. I hope that most people don't work for a boss that distrusts them. And I really appreciate a desire to have a company culture (but I would argue that not many people know how to achieve it). I can also understand that some people focus better on their job when they are removed from the distractions in their home.

But I suspect that tradition plays a major part in it. Let's face it, the major enabler of working remotely, the Internet, is still very new. And high-speed Internet in the home is still just a dream in many parts of the United States. It's going to take a while before working from non-office locations becomes "normal" in the minds of business decision-makers.

And let's not forget that many jobs don't make sense for remote work (yet).
  • Plumbers
  • Automotive technicians
  • Barbers
  • Pastry chefs
  • Landscapers
  • Lumberjacks
I am working from home when I make these comics. But I'm not a good example of anything.

Amphibian.com comic for 15 May 2015

Wednesday, May 13, 2015

MIME Type is Important for REST

Today's comic makes fun of MIME (or Internet Media) types, but they can a very useful part of web applications that make use of REST.

MIME started out as a way to tell email clients what type of file you'd attached to a message, but have become much more useful. Every time your web browser makes a request for content, it sends along a header called "Accept" which is a list of data types it can process. Typically, this includes HTML, JavaScript, and images. The server, as part of its response, includes a header called "Content-Type" which specifies what is actually being returned. The two sides usually find some common ground.

When making REST web services, being a little more particular about your MIME types can open up new possibilities. REST stands for Representational State Transfer. When a human being is driving a web browser and asks for a resource (a.k.a. a web page or URL), they expect the server to transfer a representation of the current state of that resource. That representation is often HTML or an image because that's what humans can understand (well, assuming the browser turns the HTML into something pretty first). But the real power of REST web services is how it opens things up to the possibilities of machine-to-machine communication. Let's say a computer program requests the same resource as the human. The machine would have an easier time dealing with it if it could be in a different representation other than HTML, such as JSON. Two representations, one resource.

And that's where the MIME type comes in. When making a web service call, specifying exactly what you can deal with (your desired representation of that resource) lets the server know if it should give you HTML, an image, JSON, or something else. When serving requests, explicitly setting the content type of the response lets the client know if it should even try to process it.

To help visualize it, I've created a demo. The following Node/Express server has a single route, /pizza. Depending on what the client wants, it will return either a picture of a pizza, a JSON string describing a pizza, or an HTML snippet describing a pizza.

var express = require("express");
var fs = require("fs");
var app = express();

app.get("/pizza", function(req, res, next) {

    if (req.accepts("image/jpg") === "image/jpg") {

        fs.readFile("pizza.jpg", function(err, data) {

            if (err) {
                res.sendStatus(500).send(err);
            } else {
                res.setHeader("Content-Type", "image/jpg");
                res.send(data);
            }

        });

    } else if (req.accepts("application/json") === "application/json") {

        res.setHeader("Content-Type", "application/json");
        res.send({
            size: "large",
            toppings: ["pepperoni", "mushrooms"]
        });

    } else if (req.accepts("text/html") === "text/html") {

        res.setHeader("Content-Type", "text/html");
        res.send("<p>pizza with<ul><li>pepperoni</li><li>mushrooms</li></ul></p>");

    } else {
        res.sendStatus(406);
    }

});

//------------ static content
app.use(express.static("public"));

// ------------ start listening
var server = app.listen(3000, function() {
    console.log("listening on port %d", server.address().port);
});

To make this happen, the route has to check what the client can accept, which is sent as part of the "accept" header. Express gives us the method req.accepts() to check that header. Unfortunately, I found it a little awkward to use. You pass a string or an array of strings to req.accepts() and it returns either a string of the best match or undefined if there is no match. This is because browsers typically send a whole list of things as part of the accept header - after all, a browser can deal with HTML, plain text, JavaScript, and a variety of images to say the least. Express tries to help you by telling you what the browser prefers for each request if there is more than one option. That's why I did things like put the image check first (browsers accept image/* first when requesting the src of an img tag but also accept anything else!) and always check to see if the best choice exactly equals my test string.

Look at this jQuery client code:

$.ajax("/pizza", {
    headers: { Accept: "text/html" },
    success: function(data) {
        $("#d1").html(data);
    }
});

$.ajax("/pizza", {
    headers: { Accept: "application/json" },
    success: function(data) {

        var h = "<h3>got a " + data.size + " pizza with ";
        for (var i = 0; i < data.toppings.length; i++) {
            if (i > 0) h += " and ";
            h += data.toppings[i];
        }
        h += "</h3>";
        $("#d2").html(h);

    }
});

$.ajax("/pizza", {
    headers: { Accept: "application/xml" },
    success: function(data) {
        console.log("success!");
    },
    error: function() {
        console.log("error!");
    }
});

$("#d3").html("<img src=\"/pizza\" />");

Using jQuery to make Ajax calls to a single URL three different times, I specifically set the "accept" header to just one thing in each of the requests. The first request says it can accept only text/html, so the server gives me exactly that. The second request wants JSON, so the server responds accordingly and the JSON is turned into HTML on the client side. The third request wants XML, but the server doesn't support that and responds with a 406 - Not Acceptable. That response sounds backwards, since the server is telling the client that it doesn't have a way of giving it something that it can accept not that the server got something unacceptable from the client. Finally, an image tag is generated which specifies that same URL as the source and it does in fact get the JPG from the server.

Running that JavaScript code on this web page

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MIME Types</title>
</head>

<body>

  <div id="d1"></div>
  
  <div id="d2"></div>
  
  <div id="d3"></div>

</body>

<script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>

</html>

produces results that look like this (supply your own pizza image):



And that's how you can help usher in the future of machine-to-machine communications via the Internet. Make web services that serve machines as well as people. Build the Internet of Things. Now go get some REST.

Amphibian.com comic for 13 May 2015

Monday, May 11, 2015

Make Your Own QR Codes with Node

Friday's comic showed the frogs' first Bitcoin address as a QR Code framed and hanging on a tree, much like many stores have their first dollar framed. It was a real Bitcoin address, but sadly no one has sent any money to it yet.

I had a bit of a problem getting that QR Code into the comic. If you haven't been reading this blog for long, you may not know that the comics are made out of SVG images. That means I needed a SVG QR Code. No problem, right? There are a bunch of web sites that will generate QR Codes for you in SVG format (this one, this one, this one). Unfortunately, they create them a way which makes it difficult to embed inside another SVG - like my picture frame. I either ended up with nothing, or an incredibly huge file.

Why not try a different approach?

Next I used one of those free online tools to make a .PNG version of the QR Code. It was a very small file, and I could embed the image inside the SVG of the picture frame. Great! Unlike a "pure" SVG, it will pixilize a little when scaled but it's a bunch of blocks to begin with! No problem!

Unless I want it to show up on iOS devices. Bugger!

For whatever reason, the iOS browser doesn't display the image embedded in the SVG. I was, therefore, forced to return to the SVG approach. I had to do it with two separate files - one for the empty frame and one for the QR Code in it. Not exactly what I wanted, but it works.

There is some good that came out of this, however. In my research, I came upon a really nice Node module for generating your own QR Codes in PNG or SVG format. Why rely on some other website to do it for me? It's called simply qr-image, and it has no other package dependencies.

In just a few lines of code, I was able to add a feature to my website which enables me to make all the QR Codes I want. If you use the Express framework, as I do, you can make a simple app like this, just for QR Code generation:

var express = require("express");
var qr = require("qr-image");
var app = express();

app.get("/qrc", function(req, res, next) {

    var text = req.query.text;
    res.setHeader("Content-Type", "image/png");
    res.send(qr.imageSync(text, {type: "png"}));

});

// ------------ start listening
var server = app.listen(3000, function() {
    console.log("listening on port %d", server.address().port);
});

Launch that app, point your browser to

http://localhost:3000/qrc?text=some+text+here

and you should see a nice QR Code. Put anything you want as the value of the text parameter, such as URLs, Bitcoin addresses, secret codes, etc. If you don't want to type the URL out manually, set up a simple web page form that submits the data. Or create your own custom routes that encode data from another source. The possibilities are endless. Well, maybe not endless. But certainly very numerous.

In today's comic, the frogs are trying to find something else that is endless...

Amphibian.com comic for 11 May 2015

Friday, May 8, 2015

Editable Text on Your Web Pages with X-editable

Was He Whig? No, WYSIWYG!
Right behind pictures of frogs, speech balloons are the next most important part of my web comic. I've discussed previously how the balloons in my comic are really just cleverly styled <P> tags. For my WYSIWYG comic editor, I needed to display them just like they appear in the comics, but easily allow me to type new text into them.

Inline editing of text on a page can be a helpful addition to many types of web applications that rely on human-entered data. It's not just about speech balloons. If you need to use a feature like this as well, consider X-editable, the JavaScript/CSS library I selected for my comic editor.

X-editable is a great package to use when you need inline editing and your site uses jQuery. It gets even better if you also use jQuery UI and/or Bootstrap, but it can be a little confusing to set up properly. First of all, it offers multiple download packages depending on if you want to use it with just jQuery, jQueryUI, or two different version of Bootstrap. My comic uses jQuery and Bootstrap 3, so my examples are based off of that package.

Here is the HTML for a simple demo.

<!doctype html>

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Editable Text</title>
  <link rel="stylesheet" href="css/bootstrap.min.css">
  <link rel="stylesheet" href="css/bootstrap-editable.css">
  <style>

    p {
        border: 1px solid #CCCCCC;
        margin: 10px;
        padding: 5px;
        width: 250px;
    }

    .editable-clear-x {
        background: url('clear.png');
    }

    .editableform-loading {
        background: url('loading.gif');  
    }

  </style>
</head>

<body>

  <p id="e12" class="canedit">This is some text you can edit.</p>

  <p id="e13">This is some text you can't edit.</p>

</body>

<script src="jquery-1.11.2.min.js"></script>
<script src="bootstrap.min.js"></script>
<script src="bootstrap-editable.min.js"></script>

</html>

For stylesheets you'll need Bootstrap (bootstrap.min.css in my example) and X-editable (bootstrap-editable.css). You'll also need the jQuery JavaScript, Bootstrap's JavaScript, and then finally X-editable's JavaScript.

By default, X-editable tries to find some images in the "../img/" path. That will only work if you keep your static images in a directory called "img" which is sibling to the "css" directory where you keep the X-editable CSS file. That wasn't an option for me, so I get around it by just copying the "clear.png" and "loading.gif" images to my preferred location and then overriding the CSS for those elements with my own background URL path.

In my example I have two sections of text, one that should be editable and other other not. To make the text editable inline, just execute the following JavaScript:

$(".canedit").editable({
    type: "text",
    mode: "inline",
    escape: false
});

It finds all the elements with the canedit class and makes them editable. The options object passed indicates that the type of editor will be a simple text box, using inline mode (a popup box is the alternative), and the contents will not be escaped. "Escaping" in this context means getting the value via jQuery's .text() vs. .html(). I choose not to escape so that I can put links and other HTML markup in the speech balloons. A full list of options can be found in the documentation. After executing that code, the editable text will be indicated by a blue dotted underline. Clicking on the text will replace it with a text box and some buttons. Type something new and hit enter or click the check and it turns back into plain text with the new value.

X-editable has a whole set of features that enable it to automatically POST updates to the text to a server as soon as the changes are made, but my comic editor uses a different model. I arrange all my images and text and then click on a "Save" button that reads through the DOM and builds a complete JSON representation of the comic. For speech balloons, I just do something like this:

var balloons = [];
$('p.editable').each(function(idx, elem) {

    var b = {
        // ...
        // other balloon data
        // ...
        text: $(elem).html()
    };
    balloons.push(b);

});

That finds all the editable text (in my comic editor, all <p> tags inside cells are editable), grabs the contents, creates a "balloon" object out of it, and adds it to the list of balloons.

If you're wondering what "other balloon data" I need to collect, consider that editable <p> tags can also be made draggable and resizable with jQuery UI (see my previous post where I did it with images) which enables me to easily position, size, and edit the frog speech balloons. So in addition to the actual contents of the balloons, I also read their top, left, and width CSS attributes just like I do for images.

There are lots more ways you can use X-editable in your applications. It supports built-in validation functions so you can check the input before accepting it. For example, execute this JavaScript on the demo page:

$(".canedit").editable({
    type: "text",
    mode: "inline",
    escape: false,
    validate: function(value) {
        if (value.match(/cat/gi)) {
            return "no talking about cats!";
        }
    }
});

Now you won't be able to enter "cat" in the text area. If you try it, you'll get a warning message and the edit won't complete. Your only options are to remove the cat or cancel the edit.

I mentioned above how X-editable has the capability to POST updates back to a server as soon as they are made. I don't use that capability at all, but it also lets you replace that behavior with your own custom handler. Instead of sending the data to a server via Ajax, you could send it via Websockets or update a client-side data model. To take advantage of this ability, specify a function instead of a string as the url parameter. The only catch is that the function has to return a Deferred object so the rest of the X-editable code can process it the same as a jQuery Ajax call. But you should like using Deferreds anyway, right? Check out this code:

$(".canedit").editable({
    type: "text",
    mode: "inline",
    escape: false,
    validate: function(value) {
        if (value.match(/cat/i)) {
            return "no talking about cats!";
        }
    },
    url: function(params) {
        var d = new $.Deferred;
        console.log("id of element changed: " + params.name);
        console.log("new value: " + params.value);
        setTimeout(function() {
            // to simulate some asynchronous processing
            d.resolve();
        }, 500);
        return d.promise();
    }
});

In your custom function, you can access the new value of the element in params.value. If the element that was edited had an id attribute, as my example does, it will be accessible in params.name. If you want to be able to tell your fields apart, you should probably make sure they all have ids! Finally, my example just sets a 500 millisecond timeout before resolving the Deferred, but if the action you need to take has no asynchronous component you could call d.resolve() immediately.

With all of these great tools, creating WYSIWYG editors for web content, even complex content like a comic, is easier than ever before. Now if only writing jokes for the comics was somehow made easier...

Amphibian.com comic for 8 May 2015