Friday, February 28, 2014

Adding Search and navigation by year to TimelineJS

TimelineJS is easily the coolest timeline on the planet. It has a wonderful slider and excellent animations and design. But, and this is a big but, it lacks a proper API for programmers. Making it do anything except the original uses envisaged by its designer is a pain. For example, I wanted to search the timeline. We use it for a timeline of poems, initially 720, but later this will grow to 2,200. And without search you can't find when he wrote what. Nobody is going to flick through that many entries. (And yes it does work well with 720 entries - the only problem is the time it takes to download, not render, since we don't use video or even graphics much). So I wanted to add a search. It turns out that the only practical way to do this that I could find was to add a function to the timeline.js code itself. So I added a search button, and bound my function to it:

function buildNavigation() {
...
    VMM.bindEvent("#search_button", doTimelineSearch);
    VMM.bindEvent("#year_dropdown", goToYear);
}

I also have a text box called "search_expr" to hold the search text and error messages. First I tried searching the HTML itself using jQuery but the browser caches most of the elements, so I had to use the copy of the original JSON object that TimelineJS uses to construct the slides. It was still hanging around in config.source. I added some code to the NAVIGATION section of timeline.js:

First I search from the current slide+1 to the end, then from the start to the current slide. If it is only on the current slide or not found, I write "not found" in the search box. Pretty simple. No doubt it could be written more elegantly but it was easier to debug like this, and I'm not very good at Javascript. But, hey, it works. Here it is, live. And yes it requires jQuery.

After thinking about it for a bit I added a goToYear function, which allows the user to quickly jump to an interesting section of the timeline.

Tuesday, February 25, 2014

Internationalizing Drupal 7

Recently I had to create a bilingual website in Drupal 7.26. I found information here and there but no comprehensive, up to date and detailed roadmap to say: "do this and it will all work". Here's my attempt to set the record straight.

  1. First you have to install the internationalization modules: Locale, Localization Update, Internationalization (comprising Block Languages, Content translation, Field translation, Internationalization, Language Icons, Menu Translation, Multilingual Content, Multilingual Select, Path Translation, String Translation, Synchronize translations, Taxonomy Translation, Translation redirect, Translation sets, Variable Translation, should all be enabled), Variable (comprising Variable, Variable Realm, Variable Store, should all be enabled)
  2. Now go to the Configuration page
    1. In Regional and Language click on Languages and make sure your language is the default and that they are all enabled. On Icons you can set the icon size.
    2. In Multilingual settings->Selection select "Select nodes by language" and "Select taxonomy terms by language". In the Variables tab select "Default front page". In Strings set Translatable formats to Plain text and HTML and set the source language to English.
    3. In Translate Interface load the language files of all the languages you want to support. You can get these from localize.drupal.org.
    4. In System->Site Information choose the home page for each language by first selecting the language using the language switcher and then specify a Default Front Page for it. If you generate content from nodes you don't need to do this.
  3. Now enter the administration pages and take the tabs one by one:
    1. In Structure->Menus select "Translate and Localize". Menu items with language will allow translations. Menu items without language will be localized." in the multilingual options for each menu you want to appear in multiple languages. Save. Now go back and edit each menu item of that menu that needs translating. Click the tab "Translate" and the "Add translation" to provide a translation for the missing language(s). Save.
    2. In Blocks sets the Language Switcher to appear somewhere on the page - in the header or in Primary. Now configure it so that it shows all the languages you want in "Visibility Settings" or the menu won't appear at all. In Content Types set it to appear in all. Similarly make it appear for all roles. In Footer make it translatable and select all desired languages for display. Set the correct footer text for your current language (check the URL in the browser path). If you want to translate it, switch to another language and return to Structure->Blocks->Footer. Now hit the translate button, add a translation and supply the text. Save.
    3. In Content Types: Basic page, set Language to "Hidden" or you will get a report on the language at the foot of each page.
  4. If like me you have generated content in the footer, for example, to say "Last updated on..." you can add some code to the footer (or wherever) like this:
    global $language;
    $lang_name = $language->language;
    if ( isset($node) && isset($node->changed) )
    {
        if ( $lang_name == 'it' )
            print "Ultimo aggiornamento ".format_date($node->changed);
        else
            print "Last updated ".format_date($node->changed);
    }
    

For each page in the source language you will have to "add a translation" and have it saved under the target language. I use Google Translate, which often does a passing good job. But really you need a native speaker to go over it. Now when you switch languages the menus should swap, the custom text should swap and the content will swap. You may have to do some of these things in a different order, since they are all interdependent. Good luck, and I hope I haven't left anything out. If I have, then Google it!

Sunday, February 16, 2014

TimelineJS configured dynamically with a string

TimelineJS is probably the prettiest and most reliable timeline software that works over the Web. Unfortunately there are some limitations: first that it only handles about 100 entries, although we regularly use much more than that and its performance is OK. The big problem is getting it to read from a string returned by a server. The documentation is unclear on this, so here is an attempt to spell out the technique with an example (lacking in the software distribution).

TimelineJS is configured via a JSON config object. Once timeline_config is set to this structure and TimelineJS is loaded, the config is read and the timeline gets set up automatically. To invoke it dynamically you have to specify a javascript object as input instead of a file name for the source config property. To create this JS object I put the following in a <script> element in the <head> of my HTML. For this to work you must previously have loaded both the jQuery and "../build/js/storyjs-embed.js" scripts:

function httpGet(theUrl) {
    var xmlHttp = null;
    xmlHttp = new XMLHttpRequest();
    xmlHttp.open( "GET", theUrl, false );
    if ( xmlHttp.readyState==1 )
        xmlHttp.send( null );
    return xmlHttp.responseText;
}
$(document).ready(function() {
var dataObject = httpGet("http://localhost/json/timeline/");
if ( dataObject != null )
{
    var jsObject = eval("("+dataObject+")");
    createStoryJS({
       type:       'timeline',
       width:      '100%',
       height:     '100%',
       source:     jsObject,
       embed_id:   'my-timeline'
    });
}
});

This does a http GET via an AJAX call to get the JSON, objectify it using eval, and then call createStoryJS, passing in the JSON config object. The JSON just follows the format given in the documentation. In our case we didn't want the "asset" property so we just left it out. It seems to work OK. This way you can create the timeline after the page has loaded. All it needs is a <div id="my-timeline"> in the body of the HTML for it to work.

Tuesday, February 4, 2014

Ubuntu -- disabling Recently Used in file chooser

When designing gtk someone thought it would be a great idea to help the user by providing a list of recently accessed files, and make selecting from them the default. That way you can open one of those, instead of searching for it in the file hierarchy, and so save time. But this doesn't work for a number of reasons. First, the user can't see which version of the file is being offered, just its name. Not knowing where it is located means that when it drops out of the Recently Used list you will have to search for it everywhere. I may have dozens of files with the same name on my disk. That's why we have folders: to distinguish them. Since humans like to organize information hierarchically folders tell us immediately where to find it. Not having that crutch by offering an immediate candidate actually does the user a disservice. In order to get out of the "Recently Used" list you have to click away every single time. It drives me and quite a few other people nuts. I doubt that even the designer of this feature actually uses it.

So in Ubuntu there are two possible strategies for fixing the problem:

  1. Disable "Recently Used" altogether
  2. Make the default not be Recently Used but Home.

1. looks the most attractive. Unfortunately "Recently Used" is hard-wired into the open file dialog. Short of patching gtk you can't remove it. And I wouldn't recommend patching it, even though you can find one here. Next time you update your system the fix will disappear, if you don't break your current version, which won't be the same as the one assumed by the patch. So, forget this.

2. This is an easy configuration setting. Pity that the designers of gtk didn't also provide a "recentlyused=disabled" option, but whatever. In home .config/gtk-2.0/gtkfilechooser.ini change StartUpMode=recent to StartUpMode=cwd, and then your current working directory or home will be where your heart is. By the way I use Ubuntu 13.10, so this works on the latest system.

If you want to simply prevent files from being tracked in recently used just disable it:

rm ~/.local/share/zeitgeist/recently-used.xbel
touch ~/.local/share/zeitgeist/recently-used.xbel
sudo chattr +i ~/.local/share/zeitgeist/recently-used.xbel

or as someone else suggested, instead of touch-ing and chattr-ing it, just make it a directory:

mkdir ~/.local/share/zeitgeist/recently-used.xbel