Archive for December 26th, 2010

Update: Connie Wiolowan of allanguages.info has made some comments and I've updated the post to reflect them. Thanks!

Connie also created a jQuery plugin for the virtual keyboard; see the comment for details.

I like Ilya Lebedev's JavaScript VirtualKeyboard a lot. It's clever, got an elegant UI, allows you to remap the physical keyboard to any language you want, and has an active and responsive support forum. But it's got a few quirks that make it hard to use with jQuery.

  • Virtually no documentation. You have to read the source to get anywhere. But here's a brief introduction (assuming you use jQuery):
    1. Download the package from SourceForge, unzip the tarball and put the whole folder (don't mess up the organization!) somewhere accessible.
    2. Include the script with <script type="text/javascript" src="/path/to/files/vk_loader.js" ></script>.
    3. Have a blank <div> in your HTML that will house the keyboard. I'll use <div id="jsvk" /> in these instructions.
    4. Have one or more <input type="text" >s or <textarea>s that will accept the keyboard input.
    5. In your $(document).load function, include the statement VirtualKeyboard.show($('input')[0], $('#jsvk')[0]).
    6. Other useful functions include VirtualKeyboard.hide() that hides the keyboard and removes it from the DOM, VirtualKeyboard.attachInput($('input')[0]) that leaves the keyboard visible where it was but associates a new input element with it, and VirtualKeyboard.detachInput() that leaves the keyboard visible but removes the association with an input element.
    7. VirtualKeyboard.open is a synonym for VirtualKeyboard.show and VirtualKeyboard.close is a synonym for VirtualKeyboard.hide.
  • The VirtualKeyboard is a Singleton. This means that you can only have one keyboard shown on a page, and the "skin" (the CSS of the keyboard) is fixed at initialization even if you move it around and associate it with different input elements. It also means that if you use the keyboard for different input elements, changing the language for one changes it for all of them. You can get around this by putting the keyboard into its own document and using iframes or popup windows, and the code includes samples for both of those.
  • The package is huge. The "compacted" version is 4 MB. Most of that is the development files, so it's not like every page downloads megabyte files (the largest downloaded file is the keyboard layout package, about 390K), but it's still huge.
  • Part of the reason that it's so big is that it does everything. Lebedev has put an entire Javascript library into this package, including its own event manager that overrides jQuery's. So you can't do $('input').click(function(evt){}) on elements that are attached to the keyboard. You have to use his EM.addEventListener(self.element[0],'click', function(evt){}).
  • Options are too clever by half: he parses the <script src="/path/to/vk_loader.js?query-string> query string to get them. So to set the initial layout and the skin, I used <script type="text/javascript" src="/path/to/vk_loader.js?vk_layout=IL%20Hebrew&vk_skin=flat_gray" ></script>. Options are:
    • vk_skin: name of folder in the /css folder in the package to use for the CSS
    • vk_layout: name of the keyboard layout. The available names are listed on the right hand side of the home page or the project developement page

But these are really minor issues for such a great package, and adapting it to use jQuery isn't that hard (I'm using my textpopup plugin as a base):


<div id="example">
	Example: <br/><input style="float:left"/><input style="float:right"/>
	<br style="clear:both"/>
</div>

var keyboard = false; // VirtualKeyboard is a singleton, so we have to make sure there's just one container
$.ui.textpopup.subclass("ui.jsvk", {
	_createBox: function(){
		if (keyboard) return keyboard;
		var self=this;
		self._super();
		keyboard = self.theBox;
		VirtualKeyboard.open(self.element[0], self.theBox[0]);
		EM.addEventListener(self.element[0],'keypress', function(e){
			if (e.getKeyCode()==27) self.hide();
		});
		return self.theBox;
	},
	show: function(){
		VirtualKeyboard.attachInput(this.element[0]);
		this._super();
	},
	hide: function(){
		VirtualKeyboard.detachInput();
		this._super();
	}
});

$('#example input:eq(0)').jsvk({position: 'bl'});
$('#example input:eq(1)').jsvk({position: 'br'});

Sitemeter works by inserting an image and iframe into your page, so that when the browser retrieves it, sitemeter can look at the referer and know what page was visited. But you can't include image links in an XML or GIF file, so I can't tell which of the bililite webservices are being visited. But behind the scenes, all the files are PHP files, so I can use cURL to get the image file as long as I can spoof the parameters so sitemeter thinks it knows where the request came from. A little experimentation came up with this:

function getSitemeterImage(){
	// the sitemeter image parameters; obtained by inspection
	$opts = array(
		'site' => 's48bililite',
		'refer' => urlencode ($_SERVER['HTTP_REFERER']),
		'pg' => urlencode('http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']),
		'rnd' => rand()/getrandmax()
	);
	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, 'http://s48.sitemeter.com/meter.asp?'.http_build_query($opts));
	curl_setopt($ch, CURLOPT_REFERER, $_SERVER['HTTP_REFERER']);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	// be nice to the server (http://inanimatt.com/php-curl.php)
	curl_setopt($ch, CURLOPT_USERAGENT, 'Bililite Webservices (http://bililite.com/webservices)');
	curl_exec($ch); // get the image to tell sitemeter that we were here
}

We don't actually use the image; all we want is to let sitemeter know that we want it.

I just started using Sitemeter for tracking page visits (everyone seems to use Google Analytics, but I've used Sitemeter before and am satisfied with what I've got). Sort of depressing seeing a "1" for the visit count, but it's new as of today. Creating a widget to put in the sidebar was straightforward, since I've already created one widget:

/*
Plugin Name: Sitemeter Widget
Plugin URI: http://bililite.nfshost.com/blog/2010/12/26/simple-sitemeter-widget/
Description: Display the sitemeter.com badge in the sidebar
Author: Daniel Wachsstock
Version: 1.0
Author URI: http://bililite.nfshost.com/blog
*/ 

// WordPress 2.8 Widget code based on http://jessealtman.com/2009/06/08/tutorial-wordpress-28-widget-api/
// and http://justcoded.com/article/wordpress-28-multi-widgets/

class Sitemeter_Widget extends WP_Widget{
  function Sitemeter_Widget(){
    $this->WP_Widget ('sitemeter', __('Sitemeter'), array(
     'description' => 'Display the Sitemeter Badge'
    ));
  } // constructor
  function widget ($args, $instance){
    extract ($args, EXTR_SKIP);
    extract ($instance, EXTR_SKIP);
    echo $before_widget.$before_title.$title.$after_title;
		echo "<!-- Site Meter -->
<script type=\"text/javascript\" src=\"http://$server.sitemeter.com/js/counter.js?site=$account\">
</script>
<noscript>
<a href=\"http://$server.sitemeter.com/stats.asp?site=$account\" target=\"_top\">
<img src=\"http://$server.sitemeter.com/meter.asp?site=$account\" alt=\"Site Meter\" border=\"0\"/></a>
</noscript>
<!-- Copyright (c)2009 Site Meter -->";
    echo $after_widget;
  } // widget display code
  function update ($new, $old){
    return array_merge ($old, $new);
  } // options update
  function form ($instance){
    extract ($instance, EXTR_SKIP);
    $this->textElement ('title', 'Title', $title);
    $this->textElement ('server', 'Server', $server);
    $this->textElement ('account', 'Account', $account);
  } // control form
  function textElement ($index, $text, $value){
    $id = $this->get_field_id($index);
    $name = $this->get_field_name ($index);
    $text = __($text); // localize
    echo "<p><label for=\"$id\">$text <input name=\"$name\" class=\"widefat\" value=\"$value\" id=\"$id\" /></label></p>";
  } // textElement
} // class Sitemeter_Widget

add_action ('widgets_init', create_function('', 'return register_widget("Sitemeter_Widget");'));

Short and sweet