summaryrefslogtreecommitdiff
path: root/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/itablet/scripts/itablet.js
blob: c6f84bcf53ef4a9598c8acf69b2eb298240210a3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */

/**
 *
 * This library implements a general user interface look and feel similar to a "well know tablet PC" :-)
 * It provides animated page transition eye-candy but is, in essence, really just a fancy tabbed window.
 *
 * It has dependencies on the following:
 * itablet.css
 * iscroll.js
 * jquery.js (jquery-1.7.1.min.js)
 *
 * author Fraser Adams
 */

/**
 * Create a Singleton instance of the iTablet user interface generic look and feel.
 */
var iTablet = new function() {
    if (!String.prototype.trim) { // Add a String trim method if one doesn't exist (modern browsers should have it.)
        String.prototype.trim = function() {
            return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
        };
    }

    /**
     * The Location inner class allows iTablet.location to be used similarly to window.location.
     */
    var Location = function(href) {
        this.href = href;
        var split = href.split("?"); // Split string by query part.
        this.hash = split[0];
        this.search = split.length == 2 ? "?" + split[1] : "";
        this.data = null;

        // Populate the data property with key/value pairs extracted from search part of URL.
        if (split.length == 2) {
            this.data = {};
            var kv = split[1].split("&"); // Split string into key/value pairs using the & is the separator.
            var length = kv.length;
            for (var i = 0; i < length; i++) {
                var s = kv[i].split("=");
                if (s.length == 2) {
                    var key = s[0].trim();
                    var value = s[1].trim();
                    this.data[key] = value;
                }
            }
        }

        Location.prototype.toString = function() {
            return this.href;
        };

        // Location.back() is mapped to the iTablet.goBack() method.
        // This allows clients to explicitly return to a previous page, useful for coding submit handlers etc.
        Location.prototype.back = goBack;
    };

    /**
     * The TextChange inner class adds support for a textchange Event on text input and textarea tags.
     * Initialise with: $.event.special.textchange = new TextChange();
     * Uses info derived from from http://benalman.com/news/2010/03/jquery-special-events/
     */
    var TextChange = function() {
        /**
         * Directly call the triggerIfChanged method on keyup.
         */
        var handler = function() {
			triggerIfChanged(this);
        };

        /**
         * For cut, paste and input handlers we need to call triggerIfChanged from a timeout.
         */
        var handlerWithDelay = function() {
			var element = this;
			setTimeout(function() {
				triggerIfChanged(element);
			}, 25);
        };

        /**
         * Trigger textchange Event handler bound to target element if the text has changed.
         */
        var triggerIfChanged = function(domElement) {
			var element = $(domElement);
            var current = domElement.contentEditable === "true" ? element.html() : element.val();
			if (current !== element.data("lastValue")) {
				element.trigger("textchange",  [element.data("lastValue")]);
				element.data("lastValue", current);
			}
        };

        /**
         * Called by jQuery when the first event handler is bound to a particular element.
         */
        this.setup = function(data) {
            var jthis = $(this);
		    jthis.data("lastValue", this.contentEditable === 'true' ? jthis.html() : jthis.val());
            // Bind keyup, cut, paste and input handlers in the .textchange Event namespace to allow easy unbinding.
			jthis.bind("keyup.textchange", handler);
			jthis.bind("cut.textchange paste.textchange input.textchange", handlerWithDelay);
        };

        /**
         * Called by jQuery when the last event handler is unbound from a particular element.
         */
        this.teardown = function (data) {
			$(this).unbind('.textchange'); // Unbind the Events linked to the .textchange Event namespace.
        }
    };

//-------------------------------------------------------------------------------------------------------------------

    var TOUCH_ENABLED = 'ontouchstart' in window && !((/hp-tablet/gi).test(navigator.appVersion));

    // Select start, move and end events based on whether or not the user agent is a touch device.
    var START_EV = (TOUCH_ENABLED) ? "touchstart" : "mousedown";
    var MOVE_EV  = (TOUCH_ENABLED) ? "touchmove"  : "mousemove";
    var END_EV   = (TOUCH_ENABLED) ? "touchend"   : "mouseup";

    // Populated in initialiseCSSAnimations()
    var ANIMATION_END_EV = "";// = "animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd";
    var TRANSITION_END_EV = "";//"transitionend webkitTransitionEnd MSTransitionEnd oTransitionEnd";

    /**
     * The general perceived wisdom is to use feature detection rather than browser sniffing, which seems a good
     * aim, but as it happens *most* of the browser abstraction is happening in jQuery and CSS but there remain
     * a few quirks, mostly for IE < 9. IE < 9 and Opera < 12 both appear not to trigger a change when radio or
     * checkbox state changes and it's not clear how to "feature detect" this, which is the main reason for
     * including the Opera version sniffing.
     */
    var IS_IE = (navigator.appName == "Microsoft Internet Explorer");
	var IE_VERSION = IS_IE ? /MSIE (\d+)/.exec(navigator.userAgent)[1] : 1000000;
    var IS_OPERA = (navigator.appName == "Opera") && (window.opera.version != null);
    var OPERA_VERSION = IS_OPERA ? opera.version() : 1000000;

    var BODY; // jQuery object for body is used in several places so let's cache it (gets set in jQuery.ready()).
    var IS_MOBILE = false; // Gets set if we detect a small display (e.g. mobile) device.

    var _mainLeft = 0;     // N.B. We need to get the actual value after the DOM has loaded.
    var _history = [];     // URL history to enable back button behaviour.
    var _scrollers = {};
    var _transitions = {}; // Map of transition names to transition functions (populate later after functions defined).
    this.location = null;

    /**
     * This public helper method renders an HTML list with up to <maxlength> list items using the <contents> function.
     * If the length of the list is less than the number added by the contents function additional list items are 
     * appended, conversely if the length of the list is greater than the number added by the contents function then
     * the list is truncated.
     *
     * The contents of the list are populated via the supplied <contents> function, which should take an index as a 
     * parameter and return an <li> with contents or false. If false is returned that item is skipped.
     * If an HTML list is supplied without a maxlength and contents function this method will simply carry out
     * reskinning of the input widgets for the supplied list (and adding radiused borders for older versions of IE).
     *
     * For some reason this method seems to run very sluggishly on IE6, is's especially noticable when doing a
     * resize. It runs fine on every other browser. I've not noticed any obvious inefficiencies but IE6 is weird..
     *
     * @param list jQuery object representing the html list (ul) we wish to populate.
     * @param contents a function to populate the list contents, this function should take an index as a parameter.
     *        and return an <li> with the required contents or false to skip (useful if filtering is needed).
     * @param maxlength the maximum number of items that we wish to populate (default is 1).
     */
    this.renderList = function(list, contents, maxlength) {
        // For IE 6 get the width. Have to use .main or .popup-container because list.innerWidth() may not be set yet.
        var listWidth = 0;

        if (IE_VERSION < 7) { // This seems to be very slow on IE6, no idea why???
            var main = list.closest(".main");
            if (main.length == 0) { // It's a list in a popup
                listWidth = Math.round(parseInt($(".popup-container").css('width'), 10) * 0.9);
            } else {
                listWidth = Math.round($(".main").width() * 0.9);
            }
        }

        var lengthChanged = false;
        var items = list.children("li");

        if (contents == null) {
            maxlength = items.length;
        } else if (maxlength == null) {
            maxlength = 1; // If no maxlength is supplied default to calling the contents function once.
        }

        var actualLength = 0; // Actual number of <li> supplied, this caters for the contents function returning false.
        for (var i = 0; i < maxlength; i++) {
            var li = false;
            if (actualLength < items.length) {
                if (contents) { // Modify list item with the new contents at index i.
                    var newItem = contents(i);
                    if (newItem) { // If the contents function didn't return false add contents to current li.
                        li = $(items[actualLength]);
                        actualLength++;
                        var active = li.hasClass("active") ? "active" : "";
                        var newItem = $(newItem).addClass(active);
                        li.removeClass().addClass(newItem.attr("class")); // Remove existing classes and add new ones.
                        li.html(newItem.html());
                    }
                } else { // If contents function not present we simply reskin the current list item.
                    li = $(items[actualLength]);
                    actualLength++;

                    if (IE_VERSION < 9) {
                        // Remove any markup used for faking :first-child, :last-child, :before, :after
                        li.removeClass("first-child last-child");
                        li.children("div.before, div.after, div.fbefore, div.fafter").remove();
                    }
                }
            } else { // If there are fewer items in the list than there are contents then append new list items.
                var newItem = contents(i);
                if (newItem) { // If the contents function didn't return false append contents as new li.
                    li = $(newItem);
                    actualLength++;
                    list.append(li);
                    lengthChanged = true;
                }
            }

            if (li) {
                // Reskin input widgets.
                Radio.reskin(li.children("input:radio"));
                Checkbox.reskin(li.children("input:checkbox"));

                // Fix several quirks in early versions of IE.
                if (IE_VERSION < 8) {
                    var anchor = li.children("a");
                    anchor.attr("hideFocus", "hidefocus"); // Fix lack of outline: none; in IE < 8

                    if (IE_VERSION < 7) { // IE6 percentage widths are messed up so need to set absolute width.
                        // 41 comes from padding: 0 30px 0 11px; 22 comes from padding: 0 11px 0 11px;
                        // 34 comes from margin-left: 5px; text-indent: 40px; minus 11.
                        var anchorWidth = listWidth - ((li.hasClass("arrow") ? 41 : 22) + 
                                                       (anchor.hasClass("icon") ? 34 : 11));
                        anchor.css({"width": anchorWidth + "px"});

                        // IE6 can't cope with multiple CSS classes so we need to merge these into a single class.
                        if (li.is(".arrow.radio")) {
                            li.addClass("ie6-radio-arrow");
                            if (li.hasClass("checked")) {
                                li.addClass("ie6-checked-arrow");
                            }
                        }
                    }
                }
            }
        }

        // If list is longer than the contents being added then trim the list so it's the same size.
        if (actualLength < items.length) {
            items.slice(actualLength).remove();
            lengthChanged = true;
        }

        // Add radiused borders for IE8 and below.
        // We have to do this after completely populating the list so first() and last() are correct.
        if (IE_VERSION < 9) {
            items = list.children("li");
            var last = items.last();
            last.addClass("last-child").prepend("<div class='before'></div>").append("<div class='after'></div>");

            if (IE_VERSION < 8)   {
                var first = items.first();
                if (IE_VERSION == 7) { // For IE7 fake :first-child:before and :first-child:after
                    first.prepend("<div class='fbefore'></div>").append("<div class='fafter'></div>");
                } else if (IE_VERSION < 7) { // For IE6 fake :first-child
                    // We're not adding radiused borders to IE6, this class provides the proper top border colour.
                    first.addClass("first-child");
                }
            }
        }

        // If the length has changed trigger an iScroll refresh on the top level page that contains the list.
        if (lengthChanged) {
            list.closest(".main").trigger("refresh"); // refresh is a synthetic event handled by parents of scroll-area
        }
    };

//-------------------------------------------------------------------------------------------------------------------
//                                                 UI Widgets                                             
//-------------------------------------------------------------------------------------------------------------------

    /**
     * Create a Singleton instance of the Radio class used to reskin and manage HTML input type="radio" widgets.
     */
    var Radio = new function() {
        /**
         * "Reskin" HTML input type="radio" items into a tablet "tick" style radio item. Note that the radio item
         * needs to have a label sibling and be wrapped in a parent <li> for this to work correctly.
         * e.g. <ul class="list"><li><label>test</label><input type="radio" name="test-radio" checked /></li></ul>
         */
        this.reskin = function(radios) {
            if (radios == null) { // if no input:radio is specified attempt to reskin every one in the document.
                radios = $("input:radio");
            }

            // Add classes to container <li> based on "template" radio buttons. This "re-skins" the radio buttons.
            radios.each(function() { // Iterate through each radio button.
                var jthis = $(this).hide();
                if (!jthis.hasClass("reskinned")) { // If checkbox has already been reskinned move on.
                    var parent = jthis.parent();
                    parent.addClass("radio");
                    if (this.checked) {
                        parent.addClass("checked");
                    }
                    jthis.addClass("reskinned"); // Mark as reskinned to avoid trying to reskin it again.
                }
            });
        };

        /**
         * Handle radio button state changes. This method is delegated to by the main handlePointer end() method.
         * Note that we are actually passing in the $("ul li.radio") jQuery object that the Event was bound to
         * rather than the actual Event object as this has already been extracted by handlePointer and is more
         * useful for the implementation of this method.
         */
        this.handleClick = function(jthis, type) {
            var checked = jthis.parent().children(".checked");
            var radio = jthis.children("input:radio"); // Select the template radio button.

            fade(jthis); // Fade out the highlighting of the selected <li>

            if (type != "click") { // If this handler wasn't triggered by a radio button click we synthesise one.
                BODY.unbind("click", handlePointer); // Prevent the synthetic click from triggering handlePointer.
                radio.click(); // Trigger radio button's click (on modern browsers this triggers change too if changed).
                BODY.click(handlePointer);
            }

            // We explicitly manipulate input:radio checked attr in the following block in case a name attr wasn't
            // specified in the HTML. With reskinned radio buttons the parent <ul> is the real container.
            if (!jthis.is(checked)) { // If the clicked item is not the previously checked item.
                // Clear any check mark on the previously selected <li> and the same on the template radio button.
                checked.removeClass("checked");
                checked.children("input:radio").attr("checked", false);

                // Mark the current <li> as checked and do the same on the template radio button.
                jthis.addClass("checked").change();
                jthis.children("input:radio").attr("checked", true);

                // For older IE/Opera triggering the click as done earlier won't trigger the change event so do it now.
                if (IE_VERSION < 9 || OPERA_VERSION < 12) {
                    radio.change();
                }
            }
        };
    };

    /**
     * Create a Singleton instance of the Checkbox class used to reskin and manage HTML input type="checkbox" widgets.
     */
    var Checkbox = new function() {
        /**
          * Return the input element associated with the specified label. This is used because the input element
          * may be associated to the label in a number of different was (via the "for" attribute, by containment etc.)
          */
        var findInputElement = function(label) {
            var input = $("#" + label.attr("for")); // First check if label for points to the input.
            if (input.length == 0) { // If not check if the label contains the input.
                input = label.children("input");
            }
            if (input.length == 0) { // Finally check if the label's next sibling is an input.
                input = label.next("input");
            }

            return input;
        };

        /**
         * "Reskin" HTML input type="checkbox" items into a tablet "switch" style checkbox. Note that the checkbox
         * needs to have a label sibling and be wrapped in a parent <li> for this to work correctly.
         * e.g. <ul class="list"><li><label>test</label><input type="checkbox" name="test-checkbox" checked /></li></ul>
         * If multiple <input type="checkbox"> elements are placed in an <li> then they will automatically be
         * reskinned as a "horiz-checkbox", this can also be done explicitly by doing <li class="horiz-checkbox">
         */
        this.reskin = function(checkboxes) {
            if (checkboxes == null) { // if no input:checkbox is specified attempt to reskin every one in the document.
                checkboxes = $("input:checkbox");
            }

            checkboxes.each(function() { // Iterate through each checkbox.
                var jthis = $(this).hide();
                if (!jthis.hasClass("reskinned")) { // If checkbox has already been reskinned move on.
                    // If there are multiple checkboxes in a container create a horizontal checkbox by adding class
                    // to parent. Note that <li class="horiz-checkbox"> may also be explicitly set in the HTML.
                    jthis.siblings("input:checkbox").parent().addClass("horiz-checkbox");

                    // Mop up where checkbox is contained by label e.g. <label>test<input type="checkbox"/></label>
                    jthis.parent().siblings("label").parent().addClass("horiz-checkbox");

                    var li = jthis.closest("li"); // Find containing li.

                    if (li.hasClass("horiz-checkbox")) { // Reskin horizontal checkbox.
                        // IE8 doesn't seem to distinguish between the selectors ul.list li:first-child:before and 
                        // ul.list li.horiz-checkbox:first-child:before when horiz-checkbox is dynamically added
                        // this stops the <li> fake radiused border being correctly positioned for horiz-checkboxes.
                        // By adding horiz-checkbox class to the <ul> too we can use a more explicit rule in the CSS.
                        li.parent().addClass("horiz-checkbox");

                        // As we use inline-block for horiz-checkbox we need to remove any whitespace text nodes.
                        li.contents().filter(function() {
                            return (this.nodeType == 3 && $.trim($(this).text()).length == 0);
                        }).remove();

                        // For horiz-checkbox we set the width of each item and add a span containing the right border.
                        // The inner span is needed so the border doesn't impact the element width calculation.
                        li.each(function() {
                            var buttons = $(this).children("label");
                            var width = 100/buttons.length;

                            buttons.css("width", width + "%"); // Using the percentage works for browsers > IE7.
                            // The child span helps position the border - see css (ul.list li.horiz-checkbox label span)
                            buttons.filter(":not(:last)").append("<span></span>");
                            buttons.first().addClass("first-child");
                            buttons.last().addClass("last-child");

                            // Early IE doesn't respect the width so reduce the width of the last item a little.
                            if (width < 100 && (IE_VERSION < 8 )) {
                                buttons.last().css("width", width - 1 + "%")
                            }

                            // Find input associated with each label and if it's checked add "checked" class to label.
                            buttons.each(function() {
                                var button = $(this);
                                var input = findInputElement(button);
                                input.addClass("reskinned"); // Mark as reskinned to avoid trying to reskin it again.
                                if (input.attr("checked")) {
                                    button.addClass("checked");
                                    if (button.is(buttons.last())) {
                                        // Use toggle-on not checked to avoid confusing IE6.
                                        button.parent().addClass("toggle-on"); 
                                    }
                                }
                            });
                        });
                    } else { // Reskin normal checkbox.
                        // Add markup to container <li> based on "template" checkboxes. This "re-skins" the checkboxes.
                        jthis.parent().
                            append("<div class='checkbox'><div class='mask'></div><div class='onoff'></div></div>");

                        // If the checkbox is checked find onoff switch and change its initial position to "on".
                        jthis.filter(":checked").siblings(".checkbox").children(".onoff").css("left", "0px");
                        jthis.addClass("reskinned"); // Mark as reskinned to avoid trying to reskin it again.
                    }
                }
            });
        };

        /**
         * Handle checkbox state changes. This method is delegated to by the main handlePointer method.
         */
        this.handlePointer = function(e, type) {
            // If triggered by a synthetic click the target is an input:checkbox otherwise it's a div.mask
            var jthis = (type == "click") ? $(e.target).siblings("div.checkbox") : $(e.target).parent();
            var onoff = jthis.children(".onoff");
            var checkbox = jthis.parent().children("input:checkbox"); // Select the underlying <input type="checkbox">
            var offsetX = onoff.offset().left - jthis.offset().left;
            var offset = -parseInt(onoff.css("left"), 10); // If we use translate we adjust by the left CSS position.
            var startX = (e.pageX != null) ? e.pageX : e.originalEvent.targetTouches[0].pageX;
            var clicked = true; // Set to false if move handler is called;

            var move = function(e) {
                var newX = (e.pageX != null) ? e.pageX : e.originalEvent.changedTouches[0].pageX;
                var diffX = offsetX + newX - startX;
                clicked = false;

                if (diffX >= -50 && diffX <= 0) {
                    setPosition(onoff, offset, diffX);
                }
                e.stopPropagation(); // Prevent iScroll from trying to scroll when we drag the switch.
            };

            /**
             * The pointer up handler is only bound when the pointer down has been triggered and so behaves like a click.
             */
            var end = function(e) {
                var pos = onoff.offset().left - jthis.offset().left;
                var duration = 300;

                if (clicked) {
                    pos = (pos == 0) ? -50 : 0;
                } else {
                    // Animation duration is between 150ms and 0ms based on position.
                    duration = (25 - Math.abs(pos + 25)) * 6;
                    pos = (pos < -25) ? -50 : 0;

                }

                setPosition(onoff, offset, pos, duration);

                BODY.unbind("click", handlePointer); // Prevent the synthetic click from triggering handlePointer.
                var currentlyChecked = checkbox[0].checked;
                if ((currentlyChecked && pos == -50) || (!currentlyChecked && pos == 0)) {
                    if (type == "click") {
                        checkbox.attr("checked", !currentlyChecked);
                    } else {
                        checkbox.click();
                        // For older IE/Opera triggering the click won't trigger the change event so do it now.
                        if (IE_VERSION < 9 || OPERA_VERSION < 12) {
                            checkbox.change();
                        }
                    }
                }
                BODY.click(handlePointer);

                if (TOUCH_ENABLED) {
                    jthis.unbind(MOVE_EV + " " + END_EV);
                } else {
                    BODY.unbind(MOVE_EV + " " + END_EV + " mouseleave");
                }
            };

            if (type == "click") {
                end(e);
            } else {
                // Bind move, end and mouseleave events to our internal handlers.
                if (TOUCH_ENABLED) { // Touch events track over the whole page by default.
                    jthis.bind(MOVE_EV, move).bind(END_EV, end);
                } else { // Bind mouse events to body so we can track them over the whole page.
                    BODY.bind(MOVE_EV, move).bind(END_EV + " mouseleave", end);
                }
            }
        };

        /**
         * Handle horiz-checkbox state changes. This method is delegated to by the main handlePointer end() method.
         * Note that we are actually passing in the $("ul li.horiz-checkbox label") jQuery object that the Event was 
         * bound to rather than the actual Event object as this has already been extracted by handlePointer and is
         * more useful for the implementation of this method.
         */
        this.handleClick = function(jthis, type) { 
            var parent = jthis.parent();
            var lastChild = parent.children("label").last();

            if (jthis.hasClass("checked")) {
                jthis.removeClass("checked");
                if (jthis.is(lastChild)) {
                    parent.removeClass("toggle-on"); // Use toggle-on rather than checked to avoid confusing IE6.
                }
            } else {
                jthis.addClass("checked");
                if (jthis.is(lastChild)) {
                    parent.addClass("toggle-on"); // Use toggle-on rather than checked to avoid confusing IE6.
                }
            }

            if (type == "click") {
                var input = findInputElement(jthis);
                input.attr("checked", !input.attr("checked"));
            } else { // If this handler wasn't triggered by a checkbox click we synthesise one.
                BODY.unbind("click", handlePointer); // Prevent the synthetic click from triggering handlePointer.
                var checkbox = findInputElement(jthis);
                checkbox.click();
                // For older IE/Opera triggering the click won't trigger the change event so do it now.
                if (IE_VERSION < 9 || OPERA_VERSION < 9) {
                    checkbox.change();
                }
                BODY.click(handlePointer);
            }
        }
    };

//-------------------------------------------------------------------------------------------------------------------
//                                                Main Event Handler                                            
//-------------------------------------------------------------------------------------------------------------------

    /**
     * This method handles the pointer down, move and up events.
     * We handle the discrete events because mobile Safari adds a 300ms delay to the click handler, in addition
     * we want to be able to un-highlight rows if we move the mouse/finger. Note that this handler is in the form
     * of a delegating event handler - we actually bind the events to the html body, this is so that if we modify
     * the DOM externally, e.g. via an AJAX update we will trigger from newly created elements too.
     * Note that the start(), removeHighlight(), resetHighlight(), touchMove() and end() methods are private.
     * This method does actually handle click events but its main job is to prevent the default navigation action
     * on href, however if the click event is a synthetic click caused by a jQuery trigger the method behaves like
     * a proper click handler.
     */
    var handlePointer = function(e) {
        // These are the *actual* selectors that we are interested in handling events for.
        var selectors = "ul.contents li, ul.mail li, ul li.radio, ul li div.checkbox, ul li.horiz-checkbox label, " + 
                        "ul li.arrow, ul li.pop, div.header a.back, div.header a.done, div.header a.cancel, " + 
                        "div.header a.menu";

        var target = e.target;
        var jthis = $(target);
        var parent;
        var prev;
        var href = jthis.attr("href");
        var chevronClicked = false; // Set true if we've clicked on a chevron (used for navigable radio buttons).
        var scrolled;    // Gets set if a page has been (touch) scrolled.
        var highlighted; // Gets set if a sidebar or mail item has been selected/highlighted.
        var startY;      // The vertical position when a touchstart gets triggered.

        var start = function(e) {
            // This block checks if the event target is one of the selectors, if not it uses jQuery closest to get the
            // first element that matches the selector, start at the current element and progress up through the DOM.
            if (!jthis.is(selectors)) {
                jthis = jthis.closest(selectors);
            }
        
            // If closest object doesn't match any of the selectors return true if it has an href else return false.
            if (jthis.length == 0) {
                return (!!href); // The href test allows the browser to add the active pseudoclass.
            }

            if (jthis.hasClass("checkbox")) { // If a checkbox then delegate to the Checkbox handlePointer() handler.
                Checkbox.handlePointer(e);
                return false;
            }

            parent = jthis.parent();

            // prev is the previously highlighted item. We search from parent's parent as we may have multiple lists.
            prev = parent.parent().find(".active");

            href = jthis.attr("href"); // Get the href of the element matching the selector.

            var TOUCHED_SIDEBAR = TOUCH_ENABLED && parent.is(".contents, .mail"); // Is this a touch on a sidebar list?

            // If the href isn't directly present then it's an <li> that contains the anchor, so we have to look further.
            if (!href) {
                var ICON_WIDTH = 45; // The width of the icon image plus some padding.
                var offset = Math.ceil(jthis.offset().left);

                // Get the pointer x value relative to the <li>
                var x = (e.pageX != null) ? e.pageX - offset : // Mouse position.
                        (e.originalEvent != null) ? e.originalEvent.targetTouches[0].pageX - offset : 0; // Touch pos.

                // If target is a clickable-icon we return immediately thus preventing any highlighting or navigation.
                if (jthis.hasClass("clickable-icon") && (x < ICON_WIDTH)) {
                    return false;
                }

                // If target is a navigable radio button then check if chevron was clicked/tapped.
                if (jthis.hasClass("radio") && jthis.hasClass("arrow") && ((jthis.outerWidth() - x) < ICON_WIDTH)) {
                    chevronClicked = true;
                }

                e.preventDefault(); // Stop the anchor default highlighting, we'll add our own prettier highlight.

                // This block highlights the selected <li> on mousedown or touchstart. For touch enabled devices we wait
                // for a short time before highlighting in case the touchstart was the start of a scroll rather than a
                // selection. If it was a scroll then the scrolled flag will get set by the touchMove handler and the
                // highlight is aborted when the timeout gets triggered.
                if (!prev[0] || prev[0] != jthis[0]) {
                    if (TOUCHED_SIDEBAR && !IS_MOBILE) {
                        // If TOUCH_ENABLED and a sidebar or mail list wait 50ms before highlighting in case the
                        // touch start is really the start of a touch scroll event.
                        scrolled = false;
                        highlighted = false;

                        setTimeout(function() {
                            if (!scrolled) { // scrolled may be set by touchMove if the list has been scrolled.
                                prev.removeClass("active");
                                jthis.addClass("active"); // Highlight the current <li>
                                highlighted = true;
                            }
                        }, 50);
                    } else { // If not TOUCH_ENABLED or a sidebar or mail list highlight immediately.
                        prev.removeClass("active");
                        // Navigable radio buttons don't highlight when the chevron is clicked.
                        if (!chevronClicked) {
                            jthis.addClass("active"); // Highlight the current <li>
                        }
                    }
                }

                href = jthis.children("a:first").attr("href"); // Get the href from the first anchor enclosed by the <li>
                href = (href == null) ? "#" : href;            // Create default href of "#" if none has been specified.
                href = href.replace(window.location, "");      // Fix "absolute href" bug found in IE7 and below.
            }

            BODY.bind(END_EV, end).unbind(START_EV, handlePointer);

            if (parent.hasClass("list") || (TOUCHED_SIDEBAR && IS_MOBILE)) {
                // For a small (e.g. mobile) displays the sidebar becomes the main menu page and touches behave
                // like normal list touches and become inactive on any touch move.
                BODY.bind(MOVE_EV, removeHighlight);
            } else if (TOUCHED_SIDEBAR && !IS_MOBILE) {
                // For a real sidebar detect touch scrolling on these items.
                startY = (e.originalEvent) ? e.originalEvent.targetTouches[0].pageY : 0;
                BODY.bind(MOVE_EV, touchMove);
            }
        };

        /**
         * If we move the mouse or finger in a list item we un-highlight and deactivate.
         */
        var removeHighlight = function(e) {
            BODY.unbind(MOVE_EV + " " + END_EV).bind(START_EV, handlePointer);
            jthis.removeClass("active");
        };

        /**
         * If we move a finger up or down in a sidebar item we reinstate the previous highlight ontouchend.
         */
        var resetHighlight = function(e) {
            BODY.unbind(END_EV).bind(START_EV, handlePointer);
            prev.addClass("active");
        };

        /**
         * This touch move handler is only bound if the initial event is a touch start event bound to a <li>
         * with a <ul> parent that has a .contents or .mail class. These lists need to be touch scrollable, but
         * they also need to have a persistent highlight on selected items. This method checks how many vertical
         * pixels have been scrolled and if it exceeds a threshold it triggers a scrolled state. Once scrolled it removes
         * any new highlighting and binds resetHighlight to touchend, which reinstates the previous highlight.
         */
        var touchMove = function(e) {
            var newY = e.originalEvent.changedTouches[0].pageY;
            if (Math.abs(newY - startY) > 7) { // Only trigger on an up/down finger movement.
                if (highlighted) { // If a new item was highlighted set the highlighting back to the previous item.
                    BODY.unbind(MOVE_EV + " " + END_EV).bind(END_EV, resetHighlight);
                } else {
                    BODY.unbind(MOVE_EV + " " + END_EV).bind(START_EV, handlePointer);
                }

                if (prev[0] != jthis[0]) {
                    jthis.removeClass("active");
                }
                scrolled = true;
            }
        };

        /**
         * The pointer up handler is only bound when the pointer down has been triggered and so behaves like a click
         * handler. If we have been triggered by a back selector or if we re-click an already selected sidebar entry
         * that has previously transitioned the we call goBack() to transition backwards otherwise we goTo(href).
         */
        var end = function(e) {
            BODY.unbind(MOVE_EV + " " + END_EV).bind(START_EV, handlePointer);

            if (!IS_MOBILE && (parent.hasClass("contents") || parent.hasClass("mail"))) {
                // Handle sidebar transitions.
                if (_history.length == 2 && href == _history[1].href) {
                    goBack();
                } else {
                    _history = [];
                    goTo(href);
                }
            } else if (parent.is("ul")) {
                if (jthis.hasClass("radio") && !chevronClicked) { // Delegate radio button state changes.
                    Radio.handleClick(jthis, e.type);
                } else { // Handle goTo page transition.
                    var classes = jthis.attr("class").split(" ");
                    var transition = slide; // Default animation.
                    // Look up the transition animation based upon the item's class
                    for (var i in classes) {
                        var current = classes[i];
                        var newTransition = _transitions[current];
                        if (newTransition != null) {
                            transition = newTransition;
                            break;
                        }
                    }
                    goTo(href, transition);
                }
            } else if (parent.hasClass("horiz-checkbox")) {
                Checkbox.handleClick(jthis);
            } else if (jthis.is(".back, .done, .cancel")) {
                goBack();
            } else if (jthis.hasClass("menu")) {
                // For mobile devices the home button allows immediate navigation back to the main menu, which may
                // be useful if several pages have been navigated through.
                _history = [_history[0], {href:"#menu?", transition:null}];
                goBack();
            }
        };

        if (e.type == "click") {
            e.preventDefault(); // Prevent the browser trying to navigate to the href itself onclick.
            if (!e.originalEvent) { // If event is triggered by calling the jQuery click() method.
                if (jthis.is("li, input:radio")) {
                    start(e);
                    end(e);
                } else if (jthis.is("input:checkbox")) {
                    var li = jthis.closest("li.horiz-checkbox");

                    if (li.length == 0) { // For a normal checkbox add pageX to the event and delegate handlePointer()
                        e.pageX = 0;
                        Checkbox.handlePointer(e, e.type);
                    } else {
                        // It's a horiz-checkbox. We need to find the label associated with the checkbox.
                        var label = jthis.parent("label"); // First check if the label contains the checkbox.
                        if (label.length == 0) { // If not look for a label containing a for matching the checkbox ID.
                            label = $("label[for='" + jthis.attr("id")+"']");
                        }
                        if (label.length == 0) { // If not assume the label is the preceding sibling (a bit fragile..).
                            var items = li.children();
                            label = $(items[items.index(jthis) - 1]);
                        }

                        Checkbox.handleClick(label, e.type);
                    }
                }
            }
        } else { // Event type is mousedown or touchstart so call main event start handler.
            start(e);
        }
    };

    /**
     * This method handles keyboard input. Its main purpose is to detect the return key being pressed and if it
     * has this method will attempt to trigger the handler bound to the right button, which should be a done/submit.
     */
    var handleKeyboard = function(e) {
        if (e.which == 13) { // Handle return key;
            // When the return key is pressed find any right button present in the header and trigger a click
            // on it, this should have the effect of triggering the done/submit handler for the form.
            var jthis = $(e.target);
            var done = jthis.closest(".main, .popup").find(".header a.right.button");
            done.trigger(START_EV).trigger(END_EV);
        }
    };

//-------------------------------------------------------------------------------------------------------------------
//                                              Page Navigation Methods                                          
//-------------------------------------------------------------------------------------------------------------------

    /**
     * This event handler handles the synthetic refresh event that may be triggered on a top level page containing
     * a scroll-area. The handler uses the id of the page to index the iScroll object then calls its refresh().
     */
    var handleRefresh = function(e) {
        var id = $(this).attr("id");
        if (_scrollers[id] != null) {
            _scrollers[id].refresh();
        }
    }

    /**
     * This method transitions to a selected destination. If the destination is "#" it simply returns, if there
     * is no history the destination page is shown otherwise we transition using an animation.
     * The ".split("?")[0]" blocks are there to cater for the case where the destination URL contains data to
     * be passed between page fragments, where the data is delimited by a "?" and separated by "&". 
     */
    var goTo = function(destination, transition) {
        iTablet.location = new Location(destination);
        var previous = (_history.length == 0) ? null : _history[0].href;

        if (destination == "#" || destination == previous) { // The second test guards against multiple clicks
            return;
        } else if (!IS_MOBILE && _history.length == 0) {
            var pages = $(".main");
            $(pages).each(function(index) {
                var jthis = $(this);
                var id = jthis.attr("id");
                if (("#" + id) == destination.split("?")[0]) {
                    jthis.show().trigger("show").find(".active").removeClass("active");
                    _scrollers[id].refresh(); // Refresh the touch scroller on the new page.
                    _history.unshift({href:destination, transition:null});
                } else {
                    jthis.hide().trigger("hide");
                }
            });
        } else {
            var currentPage = $(_history[0].href.split("?")[0]);
            var newPage = $(destination.split("?")[0]);
            transition(currentPage, newPage, false);
            _scrollers[newPage.attr("id")].refresh(); // Refresh the touch scroller on the new page. 
            _history.unshift({href:destination, transition:transition});
        }
    };

    /**
     * This method transitions back to the previous item in the history using an animation.
     * The ".split("?")[0]" blocks are there to cater for the case where the fragment URLs contain data to
     * be passed between page fragments, where the data is delimited by a "?" and separated by "&".
     */
    var goBack = function() {
        if (_history.length > 1) {
            iTablet.location = new Location(_history[1].href);
            var transition = _history[0].transition;
            var currentPage = $(_history[0].href.split("?")[0]);
            var newPage = $(_history[1].href.split("?")[0]);
            transition(currentPage, newPage, true);
            _scrollers[newPage.attr("id")].refresh(); // Refresh the touch scroller on the new page.
            _history.shift();

            // Hide virtual keyboard.
            document.activeElement.blur();
        }
    };

//-------------------------------------------------------------------------------------------------------------------
//                                                  Animations                                             
//-------------------------------------------------------------------------------------------------------------------

    /**
     * This method detects support for CSS3 animations and transitions and uses them if present. It will attempt
     * to use translate3d if present as this is most likely to be GPU accelerated and uses translate as fallback.
     * In an ideal world this would be set up in a stylesheet using media queries, but unfortunately using
     * prefixes for all of the keyframes is rather verbose and untidy and media query of translate3d only seems
     * to be supported in WebKit, so scripting is needed whatever. This method "injects" the relevant styles.
     */
    var initialiseCSSAnimations = function() {
        // TODO It'd be nicer to use feature detection but that can be hard to get right - how do we *reliably"
        // detect support for animationend and transitionend events, which are essential for these animations???
        if ((/android/gi).test(navigator.appVersion) || OPERA_VERSION <= 12.01) {
            // Android has poor CSS3 animation support so use jQuery.
            // Opera <= 12.01 doesn't have animationend which messes up state management, what's worse is that it
            // passes the animationSupported test, so just bomb out early Opera 12.01 at least fails that...
            return;
        }

        var domPrefixes = ["Webkit", "Moz", "O", "ms", "Khtml"];

        // For the following lookups ensure that prefix key is forced to lower case!!
        var transitionEndLookup = { // Lookup transitionend Event. Note prefixes are different to DOM prefixes 
			""			: "transitionend",
			"webkit"	: "webkitTransitionEnd",
			"moz"		: "transitionend",
			"o"			: "oTransitionEnd",
			"ms"		: "transitionend"
		};

        var animationEndLookup = { // Lookup transitionend Event. Note prefixes are different to DOM prefixes 
			""			: "animationend",
			"webkit"	: "webkitAnimationEnd",
			"moz"		: "animationend",
			"o"			: "oAnimationEnd",
			"ms"		: "transitionend"
		};

        var has3d = false;
        var domPrefix = "";
        var prefix = "";

        var style = $("<style/>");
        var styles = style[0].style;

        // We first check for animation-name and transform CSS support.
        var animationSupported = styles.animationName && styles.transform ? true : false;
        var animationend = "animationend";
        var transitionend = "transitionend";

        if (!animationSupported) { // If prefix free versions not present check for prefixed versions.
            var length = domPrefixes.length;
            for (var i = 0; i < length; i++) {
                if (styles[domPrefixes[i] + "AnimationName"] !== undefined) {
                    animationend = animationEndLookup[domPrefixes[i].toLowerCase()];
                    transitionend = transitionEndLookup[domPrefixes[i].toLowerCase()];

                    var domPrefix = domPrefixes[i];
                    prefix = "-" + domPrefix.toLowerCase() + "-";

                    if (styles[domPrefix + "Transform"] !== undefined) {
                        if (styles[domPrefix + "Perspective"] !== undefined) {
                            has3d = true;
                        }
                        animationSupported = true;
                        break;
                    } else {
                        return;
                    }
                }
            }
        }

        ANIMATION_END_EV = animationend;
        TRANSITION_END_EV = transitionend;

        if (animationSupported) { // Animating transforms is supported if this is true.
            // Webkit's 3D transforms are passed off to the browser's own graphics renderer so may give a
            // false positive, the test below should double check that 3d is indeed supported.
            if (has3d && prefix == "-webkit-") {
                has3d = 'WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix();
            }
            
            var s = ".sidebar, .main, .popup, .popup-window, .popup-container, ul li {" + 
                        prefix + "animation: 350ms ease-in-out;}";
            // Define the key animation styles. cssSlide simply adds these classes to trigger the animation.
            s += ".slideIn          {" + prefix + "animation-name: slideinfromright;}";
            s += ".slideOut         {" + prefix + "animation-name: slideouttoleft;}";
            s += ".slideIn.reverse  {" + prefix + "animation-name: slideinfromleft;}";
            s += ".slideOut.reverse {" + prefix + "animation-name: slideouttoright;}";
            s += ".fade             {" + prefix + "animation-name: fadehighlight;}";
            s += ".slideUp          {" + prefix + "animation-name: slideinfrombottom;}";
            s += ".slideDown        {" + prefix + "animation-name: slideouttobottom;}";
            s += ".dissolveIn50     {" + prefix + "animation-name: dissolvein50;}";
            s += ".dissolveOut50    {" + prefix + "animation-name: dissolveout50;}";

            // Helper method to render transform using translate3d if supported or translate if not.
            var renderTranslate = function(val) {
                return prefix + "transform: translate" + (has3d ? "3d(" : "(") + val + (has3d ? ", 0);" : ");");
            };

            // Implement setting the left css attribute of the supplied element using css translate.
            var cssSetPosition = function(element, offset, left, duration) {
                // Invoked when the animation completes, resets styles.
                var completeCallback = function() {
                    element.removeAttr("style").css("left", left + "px").unbind(TRANSITION_END_EV);
                };

                // If a duration is provided then use a transition to animate the transform.
                if (duration != null) {
                    element.css(prefix + "transition", prefix + "transform " + duration + "ms ease-out 0ms").
                            bind(TRANSITION_END_EV, completeCallback);
                }

                element.css(prefix + "transform",
                            "translate" + (has3d ? "3d(" : "(") + (left + offset) + "px, 0" + (has3d ? ", 0)" : ")"));
            };

            var keyframes = prefix + "keyframes ";

            // Now define the keframes for the animations.
            s += "@" + keyframes + "slideinfromright {";
            s += "from {" + renderTranslate("100%, 0") + "}";
            s +=   "to {" + renderTranslate("0, 0") + "}}";

            s += "@" + keyframes + "slideouttoleft {";
            s += "from {" + renderTranslate("0, 0") + "}";
            s +=   "to {" + renderTranslate("-100%, 0") + "}}";

            s += "@" + keyframes + "slideinfromleft {";
            s += "from {" + renderTranslate("-100%, 0") + "}";
            s +=   "to {" + renderTranslate("0, 0") + "}}";

            s += "@" + keyframes + "slideouttoright {";
            s += "from {" + renderTranslate("0, 0") + "}";
            s +=   "to {" + renderTranslate("100%, 0") + "}}";

            s += "@" + keyframes + "fadehighlight {";
            s += "from {background-color: #035de7; color: #ffffff;}";
            s +=   "to {background-color: #f7f7f7; color: #324F85;}}";

            s += "@" + keyframes + "slideinfrombottom {";
            s += "from {" + renderTranslate("0, 100%") + "}";
            s +=   "to {" + renderTranslate("0, 0") + "}}";

            s += "@" + keyframes + "slideouttobottom {";
            s += "from {" + renderTranslate("0, 0") + "}";
            s +=   "to {" + renderTranslate("0, 120%") + "}}";

            s += "@" + keyframes + "dissolvein50 {";
            s += "from {background-color: rgba(0, 0, 0, 0.0);}";
            s +=   "to {background-color: rgba(0, 0, 0, 0.49);}}"; // Setting to 0.49 not 0.5 prevents Firefox glitch.

            s += "@" + keyframes + "dissolveout50 {";
            s += "from {background-color: rgba(0, 0, 0, 0.5);}";
            s +=   "to {background-color: rgba(0, 0, 0, 0.0);}}";

            style.append(s);
            $("head").append(style); // "inject" animation styles into DOM.
            slide  = cssSlide; // Override slide method with the cssSlide version.
            fade   = cssFade;  // Override fade method with the cssFade version.
            popup  = cssPopup; // Override fade method with the cssFade version.
            setPosition = cssSetPosition;
        }
    };

    /**
     * Implement a simple colour fade animation using a look up table to fade active colour back to background.
     * This is the default fade implementation, which may be overridden if CSS3 animations are supported.
     */
    var fade = function(selected) {
        var table = [{"background-color": "#0360e8", "color": "#ffffff"},
                     {"background-color": "#337feb", "color": "#ffffff"},
                     {"background-color": "#669eef", "color": "#ffffff"},
                     {"background-color": "#99b8f0", "color": "#324F85"},
                     {"background-color": "#c0d7f3", "color": "#324F85"},
                     {"background-color": "#f6f6f6", "color": "#324F85"}];

        var stepCallback = function(i) {
            if (i < 6) {
                selected.css(table[i]); // Set style from look up table.
                setTimeout(function() {stepCallback(i + 1);}, 50); // 7 steps at 50ms per step = 350ms
            } else {
                selected.removeAttr("style"); // When the animation ends remove the styles we've just added.
            }
        };

        if (selected.is(":visible")) { // Only apply animation is element is visible.
            selected.removeClass("active");
            stepCallback(0);
        } else {
            selected.removeClass("active");
        }
    };

    /**
     * Implement a colour fade using a CSS3 animation to fade active colour back to background.
     */
    var cssFade = function(selected) {
        // Invoked when the animation completes, resets styles.
        var completeCallback = function() {
            selected.removeClass("fade").unbind(ANIMATION_END_EV);
        };

        if (selected.is(":visible")) { // Only apply animation is element is visible.
            selected.removeClass("active").addClass("fade").bind(ANIMATION_END_EV, completeCallback);
        } else {
            selected.removeClass("active");
        }
    };

    /**
     * Implement a horizontal slide from the currentPage to the newPage using jQuery animate() of the left and right
     * css properies. If isReverse is true the slide is left to right otherwise the slide is right to left.
     * This is the default slide implementation, which may be overridden if CSS3 animations are supported.
     */
    var slide = function(currentPage, newPage, isReverse) {
        var width = isReverse ? -currentPage.outerWidth() : currentPage.outerWidth();
        var left = newPage.hasClass("popup") ? 0 : _mainLeft;

        // Invoked when the animation completes, resets style and hides the old page and rebinds the START_EV.
        var completeCallback = function() {
            currentPage.hide(); // Hide the current page after animation completes.
            currentPage.css({"left": left + "px", "right": "0"}); // Reset css back to original settings.
            BODY.bind(START_EV, handlePointer); // Re-enable START_EV when transition completes.
        };

        BODY.unbind(START_EV, handlePointer); // Disable START_EV until transition completes.
        currentPage.trigger("hide"); // Trigger the hide handler *before* the animation.
        newPage.css({"left": left + width + "px", "right": -width + "px"}).show().trigger("show").
                animate({left: left, right: 0}, 350);
        var selected = newPage.find(".active").removeClass("active");
        if (isReverse) {
            fade(selected);
        }
        currentPage.animate({left: left - width, right: width}, 350, completeCallback);
    };

    /**
     * Implement a horizontal slide from the currentPage to the newPage using a CSS3 animation.
     * This should give a much smoother animation than the jQuery one and is GPU accelerated on some devices.
     * If isReverse is true the slide is left to right otherwise the slide is right to left.
     */
    var cssSlide = function(currentPage, newPage, isReverse) {
        var width = isReverse ? -currentPage.outerWidth() : currentPage.outerWidth();
        var classes = "slideIn slideOut reverse";

        // Invoked when the animation completes, resets styles and hides the old page and rebinds the START_EV.
        var completeCallback = function() {
            newPage.removeClass(classes);
            currentPage.hide().removeClass(classes).unbind(ANIMATION_END_EV, completeCallback);
            BODY.bind(START_EV, handlePointer); // Re-enable START_EV when transition completes.
        };

        BODY.unbind(START_EV, handlePointer); // Disable START_EV until transition completes.

        var reverse = isReverse ? " reverse" : "";
        currentPage.trigger("hide"); // Trigger the hide handler *before* the animation.
        newPage.show().trigger("show").addClass("slideIn" + reverse);
        var selected = newPage.find(".active").removeClass("active");
        if (isReverse) {
            fade(selected);
        }
        currentPage.bind(ANIMATION_END_EV, completeCallback).addClass("slideOut" + reverse);
    };

    /**
     * Implement a pop up to the newPage using jQuery animate() of the top, bottom and rgba css properies.
     * If isReverse is true the popup slides down otherwise it slides up.
     * This is the default popup implementation, which may be overridden if CSS3 animations are supported.
     */
    var popup = function(currentPage, newPage, isReverse) {
        fade(currentPage.find(".active"));
        var height = currentPage.outerWidth();
        var background = $(".popup-window");
        var container = $(".popup-container");

        var setOpacity = function(i) { // Animate opacity value.
            if (!IS_IE || IE_VERSION > 8) { // CSS rgba is supported by modern browsers.
                background.css({"background-color": "rgba(0, 0, 0, 0." + i + ")"});
            } else { // IE8 and below don't support rgba so use MS DXImageTransform filter.
                // This is a bit subtle, unfortunately progid:DXImageTransform messes with the fonts so we only use
                // it to animate the opacity, for the final black with 50% alpha effect we add the "smoked" style
                // which uses a png image background, the combination gives fairly smooth animation and normal font.
                var hexAlpha = (Math.round(25.6 * i)).toString(16); // Convert index to a hex alpha value.
                hexAlpha = (hexAlpha.length < 2) ? "0" + hexAlpha : hexAlpha; // Pad to two hex digits if necessary.
                hexAlpha = "#" + hexAlpha + "000000"; // Modify the alpha of a black  background.
                background.css({"filter": "progid:DXImageTransform.Microsoft.gradient(startColorstr=" + 
                                            hexAlpha + ",endColorstr=" + hexAlpha + ")"});
            }
        };

        var removeStyles = function(i) {
            if (IE_VERSION <= 6) { // For IE6 we've added other dynamic styles, so we need to preserve those.
            } else { // For every other browser we remove all dynamic styling.
                background.removeAttr("style");
            }
        };
    
        var fadeInBackground = function(i) { // Animate rgba opacity property from 0.0 to 0.5.
            if (i < 6) {
                setOpacity(i);
                setTimeout(function() {fadeInBackground(i + 1);}, 50);
            } else {
                removeStyles(); // When the animation ends remove the styles we've just added.
                background.addClass("smoked"); // Only does anything for IE < 9
            }
        };

        var fadeOutBackground = function(i) { // Animate rgba opacity property from 0.5 to 0.0.
            if (i >= 0) {
                setOpacity(i);
                setTimeout(function() {fadeOutBackground(i - 1);}, 50);
            } else {
                removeStyles(); // When the animation ends remove the styles we've just added.
                background.hide();
                newPage.trigger("show");
            }
        };

        currentPage.trigger("hide"); // Trigger the hide handler *before* the animation.
        if (isReverse) {
            // Wrapping in a timeout prevents IE8 glitching button styles when returning from :active state.
            setTimeout(function() {
                background.removeClass("smoked");
                fadeOutBackground(5);
            }, 10);

            container.css({"top": "64px", "bottom": "64px"}).
                      animate({top: height + "px", bottom: 128 - height + "px"}, 350);
        } else {
            $(".popup").hide();
            newPage.show().trigger("show");
            background.show();
            fadeInBackground(0);
            container.css({"top": height + "px", "bottom": 128 - height + "px"}).
                animate({top: "64px", bottom: "64px"}, 350);
        }
    };

    /**
     * Implement a pop up to the newPage using a CSS3 animation.
     * This should give a much smoother animation than the jQuery one and is GPU accelerated on some devices.
     * If isReverse is true the popup slides down otherwise it slides up.
     */
    var cssPopup = function(currentPage, newPage, isReverse) {
        fade(currentPage.find(".active"));
        var background = $(".popup-window");
        var container = $(".popup-container");
        background.removeClass("dissolveIn50 dissolveOut50");
        container.removeClass("slideUp slideDown");

        // Invoked when the animation completes, hides the background.
        var completeCallback = function() {
            background.hide();
            container.unbind(ANIMATION_END_EV, completeCallback);
            newPage.trigger("show");
        };

        currentPage.trigger("hide"); // Trigger the hide handler *before* the animation.
        if (isReverse) {
            background.addClass("dissolveOut50");
            container.bind(ANIMATION_END_EV, completeCallback).addClass("slideDown");
        } else {
            $(".popup").hide();
            newPage.show().trigger("show");
            background.show().addClass("dissolveIn50");
            container.addClass("slideUp");
        }
    };

    /**
     * Implement setting the left css attribute of the supplied element using standard jQuery css call.
     */
    var setPosition = function(element, offset, left, duration) {
        if (duration != null) {
            element.animate({left: left}, duration);
        } else {
            element.css("left", left + "px");
        }
    };

//-------------------------------------------------------------------------------------------------------------------
//                   Add HTML5 placeholder support to browsers that don't have native support.        
//-------------------------------------------------------------------------------------------------------------------

    var addPlaceholderSupport = function(inputs) {
        inputs.each(function() {
            var jthis = $(this);
            // Add textarea class to allow textarea placeholder to be styled differently to input placeholder.
            var classes = jthis.is("textarea") ? "placeholder textarea" :  "placeholder";

            jthis.focus(function() {
                jthis.siblings("span").hide();
            }).blur(function() {
                if (jthis.val() == "") {
                    jthis.siblings("span").show();
                }
            }).parent().append("<span class='" + classes + "'>" + jthis.attr("placeholder") + "</span>");
        });
    };

//-------------------------------------------------------------------------------------------------------------------
//         Some IE specific code to manipulate additional styles to help "pretty up" earlier versions of IE.         
//-------------------------------------------------------------------------------------------------------------------

    /**
     * For IE6 adjust the size as it doesn't seem to be computed correctly in pure CSS.
     * To be really nice we should probably compute SCROLLBAR, HEADER, PADDING but hey, it's only for IE6 :-)
     */
    var adjustSize = function() {
        var SCROLLBAR = 20;
        var HEADER = 44;
        var PADDING = 128;
        var width = BODY.outerWidth() + SCROLLBAR;
        var height = $(window).outerHeight();
        var mainWidth = width - _mainLeft;
        $(".main").css({"width": mainWidth + "px"});
        $(".main, .sidebar").css({"height": (height - HEADER) + "px"});
        $(".popup-window").css({"width": width + "px", "height": height + "px"});
        $(".popup-container").css({"width": (width - width * 0.4) + "px",
                                   "height": (height - (PADDING + HEADER)) + "px"});
        // Render each list (this reskins checkboxes & radio buttons and adds the border radius on old browsers).
        $("ul.list").each(function() {iTablet.renderList($(this));});
    };

    /**
     * Among its long list of failings IE6 doesn't properly support multiple classes. The active class is used in
     * several places, so this method overrides jQuery's addClass and removeClass in order to create IE6 specific
     * classes when the active or checked class is added or removed. It's a messy, but fairly effective approach.
     */
    var mergeClasses = function() {
        // Merge blue and back classes - this only works if blue is added statically which is how it's expected to be.
        $("a.blue.back.button").addClass("blue-back");

        var addClass = jQuery.fn.addClass; // Retrieve original jQuery addClass.
        jQuery.fn.addClass = function() {  // Override jQuery addClass.
            var jthis = $(this);
            // Execute the original method and save result.
            var result = addClass.apply(this, arguments);

            var length = arguments.length;
            for (var i = 0; i < length; i++) {
                if (arguments[i] == "checked") {
                    if (jthis.hasClass("arrow")) {
                        jthis.addClass("ie6-checked-arrow");
                    }
                }

                if (arguments[i] == "active") {
                    if (jthis.hasClass("arrow")) {
                        jthis.addClass("ie6-arrow-active");
                    }

                    if (jthis.hasClass("checked")) {
                        jthis.addClass("ie6-checked-active");
                    }

                    if (jthis.hasClass("radio") && jthis.hasClass("arrow")) {
                        jthis.addClass("ie6-radio-arrow-active");
                    } 

                    if (jthis.hasClass("checked") && jthis.hasClass("arrow")) {
                        jthis.addClass("ie6-checked-arrow-active");
                    } 
                }
            }

            return result;
        };

        var removeClass = jQuery.fn.removeClass; // Retrieve original jQuery removeClass.
        jQuery.fn.removeClass = function() {     // Override jQuery removeClass.
            var jthis = $(this);
            // Execute the original method and save result.
            var result = removeClass.apply(this, arguments);

            var length = arguments.length;
            for (var i = 0; i < length; i++) {
                if (arguments[i] == "checked") {
                    jthis.removeClass("ie6-checked-arrow");
                }

                if (arguments[i] == "active") {
                    jthis.removeClass("ie6-arrow-active ie6-checked-active ie6-radio-arrow-active ie6-checked-arrow-active");
                }
            }

            return result;
        };
    };

    /**
     * Sort out quirks for various versions of IE less than 9 - IE9 is fortunately *fairly* well behaved.
     */
    var fixIEQuirks = function() {
        if (IE_VERSION < 8) { // For IE7 and below.
            $("textarea").parent().addClass("textarea"); // Add textarea class to parent <li> - used by IE7 CSS styles.

            // Wrapping the sidebar in another div with zoom: 1 to enable "layout mode" fixes an
            // annoying page transition slide animation quirk with IE < 8.
            BODY.prepend($("<div id='sidebar-wrapper'></div>"));
            $("#sidebar-wrapper").append($("#menu"));

            // Sort out button styling and issue with :active not being switched off.
            $("a.button").attr('hideFocus', 'hidefocus').
                          append("<div class='before'></div><div class='after'></div>").
                          bind("click mouseleave", function() {this.blur();});
            $("ul.contents li a").attr('hideFocus', 'hidefocus'); // Fix lack of outline: none; in IE < 8
        }

        if (IE_VERSION < 7) { // IE6 is a real drag......
            adjustSize();   // Set css width and height as it doesn't seem to be computed correctly in pure CSS.
            mergeClasses(); // IE < 7 doesn't support multiple classes so merge them into IE specific class.
            $(window).resize(adjustSize);
        } else { // For IE7 and above just render list.
            // Render each list (this reskins checkboxes & radio buttons and adds the border radius on old browsers).
            $("ul.list").each(function() {iTablet.renderList($(this));});
        }
    }

//-------------------------------------------------------------------------------------------------------------------
//                            Initialise when DOM loads using (shorthand) jQuery.ready()
//-------------------------------------------------------------------------------------------------------------------

    $(function() {
        /*
         * These handlers help make the UI behave more like a User Interface than a web page. The selectstart handler
         * disables selection on everything other than input/textarea etc. the dragstart handler disables IE image
         * dragging, the touchmove handler disables mobile Safari web page "bounce" and the contextmenu handler
         * disables the right click context menu. In a web page disabling this stuff might be frowned on, but in
         * a web app it makes the application look and feel much more like a native app, which is rather the point.
         */
        $(document).bind("selectstart contextmenu", function(e) {
            return $(e.target).is('input, textarea, select, option');});
        $(document).bind("dragstart touchmove submit", function (e) {e.preventDefault();});
        $(document).bind("orientationchange", function (e) {scrollTo(0, 0);}); // Make sure position is OK
        $(document).keyup(handleKeyboard);

        // jQuery object for body is used in several places so let's cache it.
        BODY = $("body");

        _mainLeft = parseInt($(".main").css("left"), 10);

        //IS_MOBILE = BODY.hasClass("mobile"); // We use the class "mobile" for mobile devices.
        IS_MOBILE = (_mainLeft == 0); // For mobile devices there is no sidebar.
        if (IS_MOBILE) {
            _history = [{href:"#menu?", transition:null}];
        }

        // Prevent the browser trying to navigate to the href itself onclick then bind pointer Events to handlePointer.
        BODY.bind(START_EV + " click", handlePointer);

        // Ensure various form fields behave correctly with iScroll.
        $("input, textarea, select").bind(START_EV, function(e) {e.stopPropagation();});

        // Detect input and textarea placeholders separately as some browsers (Opera 11) have native support for
        // input placeholders but not textarea placeholders.
        if (!("placeholder" in $("<input>")[0])) { // Add HTML5 input placeholder support if not natively present.
            addPlaceholderSupport($("input[placeholder]"));
        }

        if (!("placeholder" in $("<textarea>")[0])) { // Add HTML5 textarea placeholder support if not natively present.
            addPlaceholderSupport($("textarea[placeholder]"));
        }

	    $.event.special.textchange = new TextChange(); // Add support for textchange Events on text input and textarea.

        // This block looks for scroll-areas and constructs an iScroll touch scroller. Note that scrollers are indexed
        // by the containing "main" page not by the id of the scroll-area. The latter id is only used by iScroll itself.
        $(".scroll-area").each(function(index) {
            var parent = $(this).parent();
            parent.bind("refresh", handleRefresh); // Bind the synthetic refresh event to the refresh handler
            var parentId = parent.attr("id");

            if (TOUCH_ENABLED) {
                _scrollers[parentId] = new iScroll(this);
            } else { // Adds a dummy scroller object if not a touch device.
                _scrollers[parentId] = {refresh: function() {}};
            }
        });

        initialiseCSSAnimations();
        _transitions = {"pop": popup};

        iTablet.location = new Location("#" + $(".main").attr("id"));

        if (IS_IE) {
            fixIEQuirks();
        } else {
            // Render each list (this reskins checkboxes & radio buttons and adds the border radius on old browsers).
            $("ul.list").each(function() {iTablet.renderList($(this));});
        }

        /*
         * iOS6 safari has a number of "quirks" that seem to be related to overly aggressive caching, one of these
         * these relates to attempting to reuse browser connections before the HTTP response has returned. In theory
         * this may be a good thing as it allows better "pipelining" on persistent connections, but in practice
         * if the "long polling" pattern is being used it can really mess things up for image "rollovers" when
         * dynamic state gets changed. This usually manifests itself by the active images taking an age to appear.
         * The following line works around this issue by initially adding the active state to the relevant items
         * this forces the active images to be loaded, which are then subsequently cached.
         */
        $("div.main ul.list li").addClass("active");

        $(".main, .popup-window").hide();
    });
};