| /* |
| * |
| * 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(); |
| }); |
| }; |
| |