Eli Grey

Saving generated files on the client-side

Have you ever wanted to add a Save as… button to a webapp? Whether you’re making an advanced WebGL-powered CAD webapp and want to save 3D object files or you just want to save plain text files in a simple Markdown text editor, saving files in the browser has always been a tricky business.

Usually when you want to save a file generated with JavaScript, you have to send the data to your server and then return the data right back with a Content-disposition: attachment header. This is less than ideal for webapps that need to work offline. The W3C File API includes a FileSaver interface, which makes saving generated data as easy as saveAs(data, filename), though unfortunately it will eventually be removed from the spec.

I have written a JavaScript library called FileSaver.js, which implements FileSaver in all modern browsers. Now that it’s possible to generate any type of file you want right in the browser, document editors can have an instant save button that doesn’t rely on an online connection. When paired with the standard HTML5 canvas.toBlob() method, FileSaver.js lets you save canvases instantly and give them filenames, which is very useful for HTML5 image editing webapps. For browsers that don’t yet support canvas.toBlob(), Devin Samarin and I wrote canvas-toBlob.js. Saving a canvas is as simple as running the following code:

canvas.toBlob(function(blob) {
    saveAs(blob, filename);
});

I have created a demo of FileSaver.js in action that demonstrates saving a canvas doodle, plain text, and rich text. Please note that saving with custom filenames is only supported in browsers that either natively support FileSaver or browsers like Google Chrome 14 dev and Google Chrome Canary, that support <a>.download or web filesystems via LocalFileSystem.

How to construct files for saving

First off, you want to instantiate a Blob. The Blob API isn’t supported in all current browsers, so I made Blob.js which implements it. The following example illustrates how to save an XHTML document with saveAs().

saveAs(
      new Blob(
          [(new XMLSerializer).serializeToString(document)]
        , {type: "application/xhtml+xml;charset=" + document.characterSet}
    )
    , "document.xhtml"
);

Not saving textual data? You can save multiple binary Blobs and ArrayBuffers to a Blob as well! The following is an example of setting generating some binary data and saving it.

var
      buffer = new ArrayBuffer(8) // allocates 8 bytes
    , data = new DataView(buffer)
;
// You can write uint8/16/32s and float32/64s to dataviews
data.setUint8 (0, 0x01);
data.setUint16(1, 0x2345);
data.setUint32(3, 0x6789ABCD);
data.setUint8 (7, 0xEF);
 
saveAs(new Blob([buffer], {type: "example/binary"}), "data.dat");
// The contents of data.dat are <01 23 45 67 89 AB CD EF>

If you’re generating large files, you can implement an abort button that aborts the FileSaver.

var filesaver = saveAs(blob, "video.webm");
abort_button.addEventListener("click", function() {
    filesaver.abort();
}, false);

Passive localization in JavaScript

Today, I created a passive localization JavaScript library named l10n.js. l10n.js is a JavaScript library that enables passive localization through native JavaScript methods, gracefully degrading if the library is not present. You can make Ajax applications, JavaScript libraries, etc. that can be localized but not require l10n.js to function. There is already a placeholder method for all API calls as specified in the ECMAScript specification and is present in all JavaScript engines, so when l10n.js isn’t present, your application works fine.

Demo

You can try out the online demo to see l10n.js in action. Currently, only the languages mentioned in the readme are supported, but more will eventually be added.

Usage

API

API documentation can be found in the readme. All API calls gracefully degrade, so calling them even without l10n.js loaded causes no problems.

Localizing strings

Calling toLocaleString() on every localizable string can create a lot of extra typing and bloat for sending your JavaScript down the wire. I recommend using the following helper function to localize strings. The reason I don’t define this in l10n.js is to not introduce any new globals, which keeps l10n.js a one of the JavaScript libraries least-prone to conflicts with other libraries.

var l = function (string) {
    return string.toLocaleString();
};

With this helper function, you can start writing l("Your localizable string") instead of "Your localizable string".toLocaleString(). I chosel instead of _ (an underscore), because it’s easier to spot so you can quickly skim your code to see which strings are localizable.

Variable replacement

If you don’t mind requiring l10n.js for your JavaScript application or library to function, I suggest using short variable strings instead of default strings. It saves bandwidth by decreasing the size of localization files, and it enables you to write nice, short code as such in the following.

  • document.title = l("%title.search")
    • Example results: "Seach - Acme, Inc."
  • confirm(l("%confirm.deleteAccount"))
    • Example results: "Are you sure you want to delete your account?"
  • link.href = "http://www.google" + l("%locale.tld")
    • Example results: "http://www.google.co.uk"

Often, string concatenation is used instead of replacement in JavaScript. With l10n.js, to make localization easier, you may have to use replacements instead. You might want to use a JavaScript library that implements something similar to C++’s sprintf(). A nice JavaScript implementation I’d recommend is php.js’s sprintf().

When localizations are downloaded

If you are using single localization URLs (<link rel="localization" hreflang="..." href="..." type="application/vnd.oftn.l10n+json"/>), they will only be downloaded when needed. If you are using multiple localizations in one (<link rel="localizations" href="..." type="application/vnd.oftn.l10n+json"/>), then the file will be downloaded right away, but externally linked localizations in the localization file will not be. If you provide an interface for your users to change locales, any non-loaded localization files will be loaded when necessary.

Including localizations with link elements

Multiple localizations can be included with one localization JSON file, with all of the top properties being language codes. Instead of putting all of the localized strings directly in the file, you may want to assign a specifc localization JSON URL to each locale, as to save bandwidth by only downloading locales the user needs.
The following is an example localization file for <link rel="localizations" href="path/to/localizations.json" type="application/vnd.oftn.l10n+json"/>.

{
  "en-US": {
      "What is your favourite colour?": "What is your favorite color?"
  },
  "fr": "path/to/french-localization.json"
}

Using localization files is the same as calling String.toLocaleString() with the JSON localizations object as the first parameter.

You can also include single localizations by specifying the standard HTML5 hreflang link element attribute and using a rel of localization instead of localizations with an ‘s’, as shown in the following.

<link rel="localization" hreflang="en-US" href="american-english.json" type="application/vnd.oftn.l10n+json"/>

The JSON file for the localization might look like the following.

{
    "What is your favourite colour?": "What is your favorite color?"
}

tinylog

tinylog is a minimalistic logging platform JavaScript library I created which is primarily intended for online IDEs and implementing console.log() for browsers without native consoles. There is also a lite version intended for embedding in other JavaScript libraries. One such library that embeds tinylog lite is Processing.js, which uses it to implement Processing’s println().

There are also online demos of using tinylog that you can try out. The tinylog saved log viewer demo only works in browsers that support the W3C File API which is only Firefox as of now.

Pausing JavaScript with async.js

async.js is a library that aims to make it so you don’t have to mess with callbacks when making applications in JavaScript 1.7 or higher by using the yield statement to pause function execution.

Examples

Please note that user interaction with the page is not blocked during the course of any of these examples.

node.next(eventType) method

The node.next(eventType) method would pause a function until the specified event is fired on the node that next was called on and would return the captured event object.

var listenForNextEventDispatch = function ([node, eventType], callback) {
    var listener = function (event) {
        node.removeEventListener(eventType, listener, false);
        callback(event);
    };
    node.addEventListener(eventType, listener, false);
};
Node.prototype.next = function (eventType) {
    return [listenForNextEventDispatch, [this, eventType]];
};

You could now do the following in an async()ed function to handle the next click event on the document.

var clickEvent = yield document.next("click");
// handle click event here

Asking the user for their impressions of async.js

The following code does not use any obtrusive and annoying functions like prompt or alert yet still can utilize execution-blocking features.

yield to.request("feedback", "POST", (
    yield to.prompt("What are your impressions of async.js?")
));
yield to.inform("Thanks for your feedback!");
// do more stuff here

As opposed to the following, which is functionally equivalent to the previous code but doesn’t use async.js’s blocking features.

async.prompt(
    ["What are your impressions of async.js?"],
    function (response) {
        async.request(
            ["feedback", "POST", response],
            function () {
                async.inform(
                    ["Thanks for your feedback!"],
                    function () {
                        // do more stuff here
                    }
                );
            }
        );
    }
);

That’s a lot of callbacks, all of which are implied when you use async.js.

Creating an async.js module for thatFunctionThatUsesCallbacks

async.yourMethodName = function ([aParameterThatFunctionUses], callback) {
    thatFunctionThatUsesCallbacks(aParameterThatFunctionUses, callback);
};

You could then use yield to.yourMethodName(aParameterThatFunctionUses) and immediately start writing code that depends onthatFunctionThatUsesCallbacks function after the statement.

Fonts in Processing.js

I recently implemented fully-featured cross-(HTML5-supporting)-browser loadFont() and text() functions in Processing.js. This implementation does not suffer from the limitation of the old implementation that only supported SVG fonts and writing text from already-installed fonts only worked recent Mozilla-based browsers. I also revamped the entire library, fixing a few hundred errors, changing and optimizing many Processing method implementations, and getting rid of all of the implied global variable leaks. Included is an image gallery of the same Processing program being run using Processing and Processing.js. The one showing only the outputted image was saved directly from a canvas element. You can also try out fonts in Processing.js at this demo page. The demo page does not use the same font as in the screenshot but it is similar. If your browser does not support @font-face CSS rules but does support the canvas text API, your system default monospace font will be used instead.

processing-js-loadFont-test-150x150 processing-loadFont-test-150x150

Another E4X DOM library

I just made another E4X DOM library, but this one is intended to implement the optional features in the ECMA-357 standard that are not implemented by Mozilla in JavaScript. The library is named e4x.js due to, for the most part, it only implementing stuff already specified in E4X. The only difference between the standard and my implementation is that the XML.xpath(xpathExpression) method also supports numeric, string, and boolean result types.

E4X DOM toolkit

I have created a small JavaScript toolkit named e4x-dom.js for making it easy to manipulate the DOM with E4X.

The following methods are implemented on XML objects by e4x-dom.js:

node()
Returns the HTML node representation of the XML.
over(element or selector)
Either overwrites element or every element matched by the CSS selector with node().
overId(id)
Same as over(document.getElementById(id)) but also preserves the the id of the element being overwritten in the element replacing it.
fill(element or selector)
Removes every child node of element or every element matched by the CSS selector. Then node() is appended to all of the emptied elements.
fillId(id)
Same as fill(document.getElementById(id))
appendTo(element or selector)
Appends node() to element or every element matched by the CSS selector.
appendToId(id)
Same as appendTo(document.getElementById(id)).
insertBefore(element or selector)
Inserts node() before element or every element matched by the CSS selector.
insertBeforeId(id)
Same as insertBefore(document.getElementById(id)).
insertAfter(element or selector)
Inserts node() after element or every element matched by the CSS selector.
insertAfterId(id)
Same as insertAfter(document.getElementById(id)).

The following are examples of using the toolkit:

var img = <img
 src={prompt("Enter an image URI")}
 alt={prompt("Describe the image")}
 id="foobar"
/>;
img.appendTo(document.body);
 
<h1>The image was <em>removed</em>.</em>.overId("foobar");
<![CDATA[And then this text node filled the header]]>.fill("#foobar");
 
// the CDATA isn't itself put into the document but the data inside it is escaped so it
// works in HTML and XHTML
<h1><![CDATA[<html> in here is <escaped>]]></h1>.insertBefore(document.body.firstChild);
 
// if you just want to create an element quickly, do xml.node()
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"/>.node(); // SVG node
<div/>.node(); // div node
 
// DOM methods are forwarded to .node()[method]()
<canvas/>.getContext("2d");
<foo bar="baz"/>.getAttribute("bar") === "baz"; // same as getting .@bar
 
// mass-modifications with CSS selectors
<strong>Absolute links are <em>not</em> allowed</strong>
 .over('a[href^="http://"], a[href^="https://"], a[href^="//"]');
 
if (user.hasEditPrivileges) {
 // wikipedia-style "put an edit link at the start of each section"
 <a href={"/edit?page=" + page.id}>[edit]</a>.insertAfter('h2');
}
 
// <![CDATA[]]> nodes directly accessed are converted to text nodes:
 
<![CDATA[
 This is a text node.
 <html> is escaped inside it.
]]>.appendTo("#some-element");

mumbl

mumbl is a JavaScript library that makes it easy to play music and create playlists on web pages.

Demo

A demo is included with mumbl but if you dont want to download it, there is also an online demo.

Please note that mumbl is not the player in the demo. mumbl is the back-end and the demo is just an example of using mumbl.

Supported Platforms

Supported Platforms

  • HTML5
    • Firefox 3.5+
    • Google Chrome 4+
  • SoundManager 2 (version 2.95b.20100323+)
    • Firefox 1.5+
    • Opera 10+
    • Google Chrome 1+
  • Songbird 1.4+

API

API documentation can be found in the readme.

Roadmap

  • 0.1.1
    • Better error handling.
    • loaderror event.
  • A while after version 0.1 is released
    • Create a simple library that makes all MP3, OGG, WAV, etc. links be able to be played using mumbl.
    • Make the demo mumbl-powered music player (it will be renamed “mumblr”) portable and reusable.
    • Remove jQuery dependency from mumblr.
    • Make the track title display scroll (maybe using a <marquee>) when it overflows.
  • Version 0.2
    • Full compatability with every major browser.
  • The distant future (maybe version 1.0)
    • Create a simplified flash audio back-end for mumbl that integrates much more nicely and has a smaller file size than SoundManager2.

Edit Page Jetpack Feature

I have created a Jetpack feature called Edit Page to see what developing a Jetpack feature would be like. As the name implies, it lets you edit any web page in one click. Right clicking on the Edit Page button added to the status bar toggles spellcheck. To change the default spellcheck preference for the Jetpack feature, go to about:config and search for the jetpacks.editpage.spellcheck boolean preference to toggle it. The following is the code I used to implement version 0.1 of the Jetpack feature:

While creating this Jetpack feature, I ran into various quirks in the Jetpack platform:

  • Jetpack features are handled as JavaScript 1.6 and not 1.8 even though Firefox supports JavaScript 1.8. Update: This is due to a Firefox 3.0.x bug that is fixed in Firefox 3.5.
  • An HTML status bar widget added with jetpack.statusBar.append() is a complete HTMLDocument but it doesn’t support any features that involve the id attribute, like document.getElementById() and CSS’s #id syntax. This may be fixable by including a doctype in the HTML (I didn’t because it’s invalid E4X) but the point is that this is an HTML snippet, which should default to some HTML or XHTML doctype.
  • jetpack.notifications.show() doesn’t allow skipping options (passing null or undefined) without falling back on defaults (Prism’s API, platform.showNotification(), allows it). The following code should fix the function to allow this kind of use:
jetpack.notifications.show = function (message) {
    var body = message,
    title = "Jetpack Notification",
    icon = null; // Mozilla favicon is http://www.mozilla.org/favicon.ico
    if (typeof message == "object") {
        body = message.body;
        if ("title" in message) {
            title = message.title;
        }
        if ("icon" in message) {
            icon = message.icon;
        }
    }
    try {
        Components.classes['@mozilla.org/alerts-service;1']
          .getService(Components.interfaces.nsIAlertsService)
          .showAlertNotification(icon, title, body);
        return true;
    } catch (e) {
        console.log("Unable to display notification:", message);
        return false;
    }
};