Technetra

Archive for August, 2009

Countdown 2.0: Supporting HTML5 Cache

Monday, August 17th, 2009

Taking advantage of HTML5 caching in an iPhone Webapp can eliminate the need to employ cumbersome data URLs for standalone or offline style applications. The offline storage facilities of HTML5 have been around for the iPhone since OS 2.1 and can, for devices running recent OS releases, significantly reduce the complexity of deploying offline Web applications. Caching also provides the potential for automatic refresh when the standalone Webapp comes back online.

Figure 1: Main Screen (in Full-Screen Mode)

Figure 1: Main Screen (in Full-Screen Mode)

Figure 2: Settings Screen (Full-Screen)

Figure 2: Settings Screen (Full-Screen)

Figure 3: SpinningWheel Date Picker (Full-Screen)

Figure 3: SpinningWheel Date Picker (Full-Screen)


So we “trained” the network connected version of our Countdown Webapp to use HTML5 caching (it already used HTML5 localStorage facilities). The “training” steps were very simple:

  1. Create a manifest which lists all the resources needed by the Web application to work offline. This includes the JavaScript, CSS and image files referenced by the application and its included packages (iUI and SpinningWheel). See Listing 1.
  2. Attach the URL of this manifest to the manifest attribute of the Webapp’s HTML tag. See Listing 2.
  3. Ensure that when the manifest resource is retrieved by the browser, the manifest is served as a text/cache-manifest mime-type by the HTTP server. In our case this meant adding
    text/cache-manifest manifest

    to our Apache mime.types configuration file. This is necessary for the moment since the listing of standard media types from IANA, as incorporated by Apache 2.2+, does not yet include this mime-type by default.

In addition, since Countdown is designed to be a standalone application, we added an apple-mobile-web-app-capable meta tag key in order to run in full-screen mode, removing the standard Safari URL and Button bars when launched from the Home Screen.

Countdown 2.0

You can try out this new version of Countdown with your iPhone. Be sure to add the application to your Home Screen using the bookmark button at the bottom of the initial Safari screen. This is necessary in order actually to view the Webapp in full-screen mode. The Webapp may be partly functional under some other browsers like Firefox and Chrome (no support for the SpinningWheel date picker).

You can download the new project files from here.

A big thanks to Sean Gilligan (co-maintainer of the iUI Framework) for suggesting these improvements to Countdown Webapp.

Listing 1: Cache Manifest for Countdown iPhone Webapp (countdown.manifest)

CACHE MANIFEST
iui/iui.css
iui/iui.js
spinningwheel/spinningwheel.css
spinningwheel/spinningwheel.js
countdown.css
countdown.js
countdown_touch_icon.png
iui/listArrowSel.png
iui/loading.gif
iui/selection.png
iui/toolbar.png
iui/toolButton.png
iui/blueButton.png
iui/backButton.png
iui/whiteButton.png
iui/redButton.png
iui/grayButton.png
iui/listGroup.png
iui/listArrow.png
iui/pinstripes.png
iui/toggle.png
iui/toggleOn.png
iui/thumb.png
spinningwheel/sw-header.png
spinningwheel/sw-button-cancel.png
spinningwheel/sw-button-done.png
spinningwheel/sw-slot-border.png
spinningwheel/sw-alpha.png

Listing 2: Adding manifest URL to manifest attribute of HTML tag & removing top and bottom banners with apple-mobile-web-app-capable META tag (countdown.html)

...
<html manifest="countdown.manifest">

  <head>
    <title>Countdown</title>
    <link rel="apple-touch-icon" href="countdown_touch_icon.png" />

    <meta name="viewport"
      content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
...

Countdown iPhone Webapp

Monday, August 10th, 2009

What is it?

Countdown is an iPhone app started at this year’s iPhoneDevCamp 3 which was held at Yahoo! from July 31st through August 2nd.

Countdown is a simple multi-color timer that charts how many days, hours, minutes and seconds are left until a selected target event arrives. For example, this may be a fun (or frustrating) way to count how long you must wait until your next birthday. Built-in events also include “Midnight” and “New Year’s”. There is a “Settings” screen that allows you to configure some of the parameters of the program. One of the most interesting configuration elements is a date picker that uses the hardware accelerated “SpinningWheel” from cubiq.org.

Figure 1: Main Screen

Figure 1: Main Screen

Figure 2: Settings Screen

Figure 2: Settings Screen

Figure 3: SpinningWheel Date Picker

Figure 3: SpinningWheel Date Picker


What does the app do?

Countdown illustrates

  1. Webkit techniques to build a “native-look-and-feel” iPhone Webapp using JavaScript and CSS,
  2. the latest stable iUI (iui-0.30), and
  3. integration of the latest (updated) SpinningWheel, a powerful JavaScript tool that resembles the native Picker View widget (UIPickerView) on the iPhone. The SpinningWheel tool provides a fast and flexible user interface for many kinds of data input and visualization. It allows multiple columns or slots (we are using 3) to be defined to represent complex values like dates and other tabular data. Interaction with the widget is fully hardware accelerated.

Also, we wanted to create a version of our webapp which could be installed as a stand-alone application that would not require access to a network to run. This means that all data items, including images, stylesheets and scripts, must be bundled with the application beforehand. Dynamic data, such as user-updated application settings, must be kept in local storage by the application across multiple launches and even across power cycles.

What issues did we face?

There were some challenges:

  1. Integration of iUI and SpinningWheel:

    Integrating iUI with other packages requires some understanding of how iUI manages HTML containers used for displaying successive screens on the iPhone. Essentially the CSS rules of iUI turn off any unselected, top level elements within the body of an HTML page. In order to get SpinningWheel to work properly with iUI, our application needed to define a CSS rule to ensure display of the dynamically created primary container of the SpinningWheel widget. This container has the id sw-wrapper and is added as a child of the screen’s body tag when the widget is opened. A short CSS rule in our application’s stylesheet (loaded after the iUI stylesheet) was enough to override iUI’s default behavior.

    #sw-wrapper { display:block; }

    This simple addition to our stylesheet enabled SpinningWheel to be used with iUI without any modification to either package.

    In addition, we had to reposition the SpinningWheel’s view frame, sw-frame for our application’s layout. The values for portrait and landscape orientations that worked for Countdown were

    body[orient="portrait"] #sw-frame { bottom:112px; }
    body[orient="landscape"] #sw-frame { bottom:8px; }

    Using SpinningWheel with its hardware accelerated animations is a lot of fun and is a great way for a user to see and select tabular data like dates.

  2. Client-side storage using HTML5:

    We used WebKit’s support for the localStorage DOM attribute of HTML5. This is W3C’s new Web Storage abstraction for JavaScript managed user data. localStorage supports persistence beyond the current session and is ideal (essential in the case of the stand-alone version of our application) for saving the application’s settings in a server independent manner. On the iPhone, the process of creating the stand-alone application changes the application’s domain origin to a null value. This value is then automatically used as the application’s Web Storage domain for all localStorage access, creating referential consistency.

  3. Converting Countdown into a stand-alone application:

    [ Note: the following section applies to this version of the Countdown webapp which was implemented using Data URLs. HTML5 Caching provides a better implementation strategy and is covered in the follow-on article "Countdown 2.0: Supporting HTML5 Cache" ]

    Converting this webapp, when implemented using Data URLs, into a stand-alone application required modification of iUI in three ways. First, iUI navigates back to screens presented earlier by updating the application’s global window location attribute with URLs from its page history cache. Due to security constraints, updating the location attribute is not allowed by Safari when running in stand-alone mode. Second, iUI uses AJAX requests to update application content. This is also forbidden by the security context imposed on a stand-alone application. This functionality can therefore be removed to save space. Third, iUI preloads images to improve performance. This is not needed in a stand-alone application.

    To address the first of these three issues, the standalone version of our application uses an iUI package modified to allow forward and backward navigation via document section ids (using the link’s hash attribute in same way the unmodified iUI does currently for forward navigation). Countdown has only two working screens, but applications that require more screens would need to enhance this approach with a simple history queue.

    To prevent violation of the stand-alone security context, the predefined AJAX GET and POST requests were removed from iUI. This means that all resources needed by the stand-alone application have to be bundled with the program when it is written. This is a constraint that all stand-alone webapps on the iPhone are burdened with anyway, so the need to remove AJAX from iUI was not surprising.

    Avoiding preloading of images required only simple changes to iUI’s JavaScript and CSS.

    Finally, converting this webapp into a stand-alone application also required constructing a simple installer. Fortunately, we were able to reuse the installer tools discussed in our earlier article “How to Deploy an iPhone Web Application”.

    It is important to note that these changes to iUI were only required for building the stand-alone version of our application. For the network connected version of Countdown, both SpinningWheel and iUI could be used without any modifications.

    1. Try Out Countdown!

      [ Please check out the new version of Countdown that uses HTML5 caching instead of Data URLs ]

      There are two versions of the Countdown application.

      Network connected version

      Try out Countdown on your iPhone.

      This network connected version of Countdown may also work in some other browsers (Safari 4+, Firefox 2+, Chrome 3.0.197+) with limited functionality. [EXPERIMENTAL]

      Stand-alone version

      Install Countdown as a “native application” on your iPhone Home Screen.

      This installable version of Countdown may also work as a bookmark (Data URL) in some other browsers (in particular Safari 4+) with limited functionality. It does not work properly in any version of Firefox. [EXPERIMENTAL]

      Download the complete Countdown application and its resources, including the stand-alone and non-stand-alone application versions.

      Note that Countdown works best on a real iPhone or simulator. However, the application can be run in a limited way on various modern browsers like Safari 4+, Firefox 3+ and Chrome/Chromium 3.0.197+. In these cases, instead of using the (iPhone specific) SpinningWheel interface, Countdown presents a standard select element for date picking. Countdown also uses persistent storage when the browser (e.g., Firefox 3.5, Safari 4+) supports Web Storage. Otherwise Countdown uses a private array which is recreated for each new instance of the application.

      Listing 1: Countdown HTML – Network connected version (countdown.html)

      <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
        "http://www.w3.org/tr/xhtml1/DTD/xhtml1-transitional.dtd">
      <!-- 
           Copyright (c) 2009 Technetra Corp
           Licensed under The MIT License
           Originally inspired by Danny Goodman's Countdown
           script in "JavaScript and DHTML Cookbook"
      -->
      <html>
      <head>
        <title>Countdown</title>
        <link rel="apple-touch-icon" href="TOUCH_ICON" />
        <meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;" />
        <link rel="stylesheet" href="iui/iui.css" type="text/css" media="all" /> 
        <link rel="stylesheet" href="spinningwheel/spinningwheel.css" type="text/css" media="all" /> 
        <link rel="stylesheet" href="countdown.css" type="text/css" media="all" /> 
        <script type="text/javascript" src="iui/iui.js"></script> 
        <script type="text/javascript" src="countdown.js"></script> 
        <script type="text/javascript" src="spinningwheel/spinningwheel.js"></script> 
      </head>
      <body>
       
      <!-- MAIN -->
      <div class="toolbar">
        <h1 id="pageTitle"></h1>
        <a class="button" id="backButton" href="#" onClick="Countdown.turnActionButtonOn('settingsButton','doMain');"></a>
        <a class="button" id='settingsButton' href="#setupForm" onClick="Countdown.turnActionButtonOff(this,'doSetup');">Settings</a>
      </div>
      <span selected="true">
        <h1 class="ctr">
          <span id="main_subtitles">
            <span>Today: <span id="today">&nbsp;</span><br /><span class="divider" id="divider"> &diams;</span></span>
            <span>Start Date: <span id="start_date">&nbsp;</span><span class="divider"> &diams;</span></span>
            <span>Event Date: <span id="target_date">&nbsp;</span></span>
          </span>
        </h1> 
        <div class="unit" id="countdown_box"> 
          <div id="days_box">
            <span id="days"></span><span class="unit_label">Days</span>
          </div>
          <div class="overlay" id="days_overlay"></div>
          <div id="hours_box">
            <span id="hours"></span><span class="unit_label">Hours</span>
          </div>
          <div class="overlay" id="hours_overlay"></div>
          <div id="minutes_box">
            <span id="minutes" style=""></span><span class="unit_label">Minutes</span>
          </div>
          <div class="overlay" id="minutes_overlay"></div>
          <div id="seconds_box">
            <span id="seconds"></span><span class="unit_label">Seconds</span>
          </div>
          <div class="overlay" id="seconds_overlay"></div>
        </div> 
        <div id="main_footer">
          <div id='update_interval_container' >(updated every <span id='update_interval_text'></span>)</div>
        </div>
        <br /><br /><br /><br /><br />&nbsp;
      </span>
      <!-- SETUP -->
      <div id="setupForm" title="Settings" class="panel">
        <h2>Target Event</h2>
        <fieldset>
        <div class=row>
          <label>Title</label>
          <input id="title_input_field" onChange="Countdown.setTitleFieldResources(this.value);" type="text" size=30 value="" />
        </div>
        <div class=row>
          <label>Event</label>
          <select id="select_event" onChange="Countdown.updateEventType(this);" style="font-size:1.0em; text-align:left;">
            <option value='newyears'>New Years</option>
            <option value='birthday'>Birthday</option>
            <option value='midnight'>Midnight</option>
            <option value='other'>Other</option>
            <option value='reset'>Reset</option>
          </select>
        </div>
        </fieldset>
        <h2>Countdown Details</h2>
        <fieldset>
        <div class=row>
          <label>Update Every</label>
          <select id="select_interval" onChange="Countdown.updateInterval(this);" style="font-size:1.0em; text-align:left;">
            <option value='1000'>second</option>
            <option value='2000'>2 seconds</option>
            <option value='60000'>minute</option>
            <option value='3600000'>hour</option>
            <option value='86400000'>day</option>
          </select>
        </div>
        <div class=row>
          <label>Date Format</label>
          <select id="select_date_format" onChange="Countdown.updateDateFormat(this);" style="font-size:1.0em; text-align:left;">
            <option value='mm/dd/yyyy'>MM/DD/YYYY</option>
            <option value='yyyy/mm/dd'>YYYY/MM/DD</option>
            <option value='dd/mm/yyyy'>DD/MM/YYYY</option>
            <option value='mm-dd-yyyy'>MM-DD-YYYY</option>
            <option value='yyyy-mm-dd'>YYYY-MM-DD</option>
            <option value='dd-mm-yyyy'>DD-MM-YYYY</option>
          </select>
        </div>
        <div class=row>
          <label>Number Padding</label>
          <select id="select_padding" onChange="Countdown.updatePadding(this);" style="font-size:1.0em; text-align:left;">
            <option value=' '>none</option>
            <option value='0'>0</option>
            <option value='-'>-</option>
            <option value='&hearts;'>&hearts;</option>
            <option value='&diams;'>&diams;</option>
          </select>
        </div>
        <div id="spinning_wheel_date_container" class="row">
          <label>Event Date: <span id="sw_date_picked"></span></label>
          <a class="button blueButton" href="#dummy" onClick="SpinningWheel_AI.openEventDate();">Change Date</a>
        </div>
        <div id="select_date_container" class="row">
        <label>Event Date:</label>
        <table style="float:right;margin-right:-27%;">
          <tr>
            <td align="left" class="setup_cell">
              <select id="select_month" onChange="Countdown.updateMonth(this);">
                <option value='-1'>Month</option>
                <option>1</option><option>2</option><option>3</option>
                <option>4</option><option>5</option><option>6</option>
                <option>7</option><option>8</option><option>9</option>
                <option>10</option><option>11</option><option>12</option>
              </select>
              <select id="select_day" onChange="Countdown.updateDay(this);">
                <option value='-1'>Day</option>
                <option>1</option><option>2</option><option>3</option>
                <option>4</option><option>5</option><option>6</option>
                <option>7</option><option>8</option><option>9</option>
                <option>10</option><option>11</option><option>12</option>
                <option>13</option><option>14</option><option>15</option>
                <option>16</option><option>17</option><option>18</option>
                <option>19</option><option>20</option><option>21</option>
                <option>22</option><option>23</option><option>24</option>
                <option>25</option><option>26</option><option>27</option>
                <option>28</option><option>29</option><option>30</option>
                <option>31</option>
              </select>
              <select id="select_year" onChange="Countdown.updateYear(this);">
                <option value='-1'>Year</option>
                <option>2009</option><option>2010</option><option>2011</option>
                <option>2012</option><option>2013</option><option>2014</option>
              </select>
            </td>
          </tr>
        </table>
        </div>
        </fieldset>
      </div>
      <!-- DONE! -->
      <a id="counter_done" class="button" onClick="Countdown.acknowledgeDone(this);" href="#dummy">Countdown is done!<br/>OK</a>
      </body>
      </html>

      Listing 2: Countdown JavaScript Implementation (countdown.js)

      /*
        Copyright (c) 2009 Technetra Corp
        Released under The MIT License
      */
      var Countdown;
      /* BEGIN SpinningWheel Application Interface */
      var SpinningWheel_AI = {
        openEventDate: function() {
          var obj = document.getElementById('select_event');
          var event_title = obj.options[obj.selectedIndex].text;
          if (event_title == "Birthday" || event_title == "Other") {
        	var now = new Date();
        	var days = { };
        	var years = { };
        	var months = { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun', 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec' };
       
        	for( var i = 1; i < 32; i += 1 ) {
        		days[i] = i;
        	}
       
        	for( i = now.getFullYear(); i < now.getFullYear()+4; i += 1 ) {
        		years[i] = i;
        	}
            SpinningWheel.addSlot(years, 'right', Countdown.get_selected_year());
            SpinningWheel.addSlot(months, '', Countdown.get_selected_month());
            SpinningWheel.addSlot(days, 'right', Countdown.get_selected_day());
            SpinningWheel.setCancelAction(SpinningWheel_AI.cancel);
            SpinningWheel.setDoneAction(Countdown.updateSWDate);
       
        	SpinningWheel.open();
          } else {
            alert("Please select 'Birthday' or 'Other' Event Type To Customize Date");
          }
        }, 
        cancel: function() {
          //document.getElementById('sw_date_picked').innerHTML = 'cancelled!';
        }
      }
      /* END SpinningWheel Application Interface */
       
      /* Countdown Closure */
      Countdown = function() {
        var $ = function(id) { return document.getElementById(id); }
        var $getInnerText = function(id) { var id = $(id); return (id.innerText? id.innerText : id.innerHTML) }
        var $setInnerText = function(id, val) { // hack for Firefox 3.5
          id = $(id);
          if (id.innerText)
            id.innerText = val;
          else
            id.innerHTML = val;
        }
        var MSSEC = 1000;
        var MSMIN = 60 * MSSEC;
        var MSHR = 60 * MSMIN;
        var MSDAY = 24 * MSHR;
        var gMy_interval_timer;
        var gSelected_day, gSelected_month, gSelected_year;
        var gTitle_input_field = "Midnight";
        var gEvent_type_text = "Midnight";
        var gEvent_type_value = "midnight";
        var gTarget_time;
        var gDone = false;
        var gTotal_yeardays = 365;
        var gStart_month, gStart_day, gStart_year;
        var gMark_month = -1, gMark_day = -1, gMark_year = -1;
        var gDate_format = "mm/dd/yyyy";
        var gPad = " ";
        var gUpdate_date_mark = false;
        var gUpdate_interval = 2000;
        var gUpdate_interval_text = "2 seconds"; 
        var gLocalStore = {};
        var gIs_iPhone = (navigator.userAgent.toLowerCase().indexOf('iphone')!=-1);
       
        window.onorientationchange = function() {
            switch(window.orientation) {
              case 0:
                document.body.setAttribute('orient', 'portrait');
                break;
              case 90:
              case -90:
                document.body.setAttribute('orient', 'landscape');
                break;
            }
            setTimeout('Countdown.countDown()', 0); 
            setTimeout(scrollTo, 100, 0, 1);
        }
       
        window.addEventListener("load", function() {
            initApp();
            window.onorientationchange();
            Countdown.countDown(); // set up initial timer values, fields and overlays
            gMy_interval_timer = window.setInterval('Countdown.countDown()', gUpdate_interval);
        }, false);
       
        var storeItem = function(item, val) {
          if (window.localStorage) {
            window.localStorage[item] = val;
          } else {
            gLocalStore[item] = val;
          }
        };
       
        var retrieveItem = function(item) {
          var val;
          if (window.localStorage) {
            val = window.localStorage[item];
          } else {
            val = gLocalStore[item];
          }
          return val;
        };
       
        var clearItems = function() {
          if (window.localStorage) {
            window.localStorage.clear();
          } else {
            gLocalStore = {};
          }
        };
       
        var actionButton = function(btn, action, dsply) {
          if (typeof btn == 'string') {
            if (btn != '') $(btn).style.display = dsply;
          } else {
            btn.style.display = dsply;
          }
          if (action != '') Countdown[action]();
        };
       
        var store_date = function(y, m, d) {
          storeItem('selected_year', y);
          storeItem('selected_month', m);
          storeItem('selected_day', d);
        };
       
        var store_update_interval = function(i, t) {
          storeItem('update_interval', i);
          storeItem('update_interval_text', t);
        };
       
        var format_date = function(date_fields) {
          var fmt = "";
          switch (gDate_format) {
            case "mm/dd/yyyy":
                fmt = [ date_fields.m, date_fields.d, date_fields.y ].join('/');
                break;
            case "yyyy/mm/dd":
                fmt = [ date_fields.y, date_fields.m, date_fields.d ].join('/');
                break;
            case "dd/mm/yyyy":
                fmt = [ date_fields.d, date_fields.m, date_fields.y ].join('/');
                break;
            case "mm-dd-yyyy":
                fmt = [ date_fields.m, date_fields.d, date_fields.y ].join('-');
                break;
            case "yyyy-mm-dd":
                fmt = [ date_fields.y, date_fields.m, date_fields.d ].join('-');
                break;
            case "dd-mm-yyyy":
                fmt = [ date_fields.d, date_fields.m, date_fields.y ].join('-');
                break;
          }
          return fmt;
        };
       
        var setOverlay = function(obj, tp, offset) {
          var mfactor, wfactor;
          offset = (offset < 0) ? 0 : offset;
          offset = (offset > 59) ? 59 : offset;
          if (window.orientation==0) {
            mfactor = 138/60;
            wfactor = 136;
            tp += 52;
            height = "58px";
          } else {
            mfactor = 260/60;
            wfactor = 256;
            height = "36px";
          }
          obj.style.top = tp+"px";
          obj.style.height = height;
          obj.style.width = (wfactor-(offset*mfactor))+"px";
          /* obj.innerHTML = obj.style.width; */
          /* obj.innerHTML = tp+"px"; */
        };
       
        var formatNum = function(num, len) {
            var d, padding;
            var fmt = "&hellip;";
            if (num < 0) return fmt;
            fmt = "" + num;
            d = len - fmt.length + 1; 
            padding = new Array(d).join(gPad);
            return padding + fmt;
        }; 
       
        var setFields = function(days, hrs, mins, secs) {
            if (gUpdate_interval_text.match(/minute/)) {
              secs = -1;
            } else if (gUpdate_interval_text.match(/hour/)) {
              secs = mins = -1;
            } else if (gUpdate_interval_text.match(/day/)) {
              secs = mins = hrs = -1;
            }
            setOverlay($("days_overlay"), $("days").offsetTop, Math.floor((days*60)/gTotal_yeardays));
            setOverlay($("hours_overlay"), $("hours").offsetTop, Math.floor((hrs*60)/24));
            setOverlay($("minutes_overlay"), $("minutes").offsetTop, mins);
            setOverlay($("seconds_overlay"), $("seconds").offsetTop, secs);
            $setInnerText("days", formatNum(days, 3));
            $setInnerText("hours", formatNum(hrs, 3));
            $setInnerText("minutes", formatNum(mins, 3));
            $setInnerText("seconds", formatNum(secs, 3));
            //alert("setFields, days="+days+", hrs="+hrs+", mins="+mins+", secs="+secs);
        };
       
        var setupSelectElementByValue = function(el, val) {
          var i, o;
          o = el.options;
          for (i=0; i < o.length; i++) {
            if (o[i].value == val) {
              el.selectedIndex = i;
            }  
          }
        };
       
        var setupSelectElementByText = function(el, val) {
          var i, o;
          o = el.options;
          for (i=0; i < o.length; i++) {
            if (o[i].text == val) {
              el.selectedIndex = i;
            }  
          }
        };
       
        var setupDate = function(etype) {
          var targ_date, start_date;
          var now = new Date();
          start_date = new Date(now);
          gStart_day = now.getDate();
          gStart_month = now.getMonth();
          gStart_year = now.getFullYear();
          var selected_hour = 1, selected_minute = 1, selected_second = 1;
          switch (etype) {
            case "midnight":
              now.setDate(now.getDate() + 1);
              gSelected_day = now.getDate();
              gSelected_month = now.getMonth() + 1;
              gSelected_year = now.getFullYear();
              store_date(gSelected_year, gSelected_month, gSelected_day);
              break;
            case "newyears":
              now.setFullYear(now.getFullYear() + 1);
              gSelected_day = 1;
              gSelected_month = 1;
              gSelected_year = now.getFullYear();
              store_date(gSelected_year, gSelected_month, gSelected_day);
              break;
          }
          targ_date = new Date(gSelected_year, gSelected_month-1, gSelected_day, selected_hour-1, selected_minute-1, selected_second-1);
          gTotal_yeardays = Math.floor((targ_date.getTime() - start_date.getTime())/(1000*60*60*24));
          $setInnerText('start_date', format_date({m:gStart_month+1, d:gStart_day, y:gStart_year}));
          $setInnerText('target_date', format_date({m:gSelected_month, d:gSelected_day, y:gSelected_year}));
          if (!gIs_iPhone) {
            setupSelectElementByText($('select_day'), gSelected_day);
            setupSelectElementByText($('select_month'), gSelected_month);
            setupSelectElementByText($('select_year'), gSelected_year);
          }
          return targ_date;
        };
       
        var initApp = function() {
          if (retrieveItem('pad')) gPad = retrieveItem('pad');
          if (retrieveItem('event_type_text')) gEvent_type_text = retrieveItem('event_type_text');
          if (retrieveItem('event_type_value')) gEvent_type_value = retrieveItem('event_type_value');
          if (retrieveItem('title_input_field')) gTitle_input_field = retrieveItem('title_input_field');
          if (retrieveItem('date_format')) gDate_format = retrieveItem('date_format');
          if (retrieveItem('selected_day')) gSelected_day = parseInt(retrieveItem('selected_day'));
          if (retrieveItem('selected_month')) gSelected_month = parseInt(retrieveItem('selected_month'));
          if (retrieveItem('selected_year')) gSelected_year = parseInt(retrieveItem('selected_year'));
          if (retrieveItem('update_interval')) gUpdate_interval = parseInt(retrieveItem('update_interval'));
          if (retrieveItem('update_interval_text')) gUpdate_interval_text = retrieveItem('update_interval_text');
          document.getElementsByTagName('body')[0].setAttribute('device', gIs_iPhone?'iphone':'unknown');
          $setInnerText('update_interval_text', gUpdate_interval_text);
          $setInnerText('pageTitle', gTitle_input_field);
          $('title_input_field').value = gTitle_input_field;
          gTarget_time = setupDate(gEvent_type_value).getTime();
        };
       
        var setEventTypeResources = function(ett, etv) {
          storeItem('event_type_text', ett);
          storeItem('event_type_value', etv);
          gEvent_type_text = ett;
          gEvent_type_value = etv;
        };
       
        return {
          get_selected_year: function() {
            return gSelected_year;
          },
       
          get_selected_month: function() {
            return gSelected_month;
          },
       
          get_selected_day: function() {
            return gSelected_day;
          },
       
          doSetup: function() {
              var i, e;
              if (gMy_interval_timer != null) {
                window.clearInterval(gMy_interval_timer);
                gMy_interval_timer = null;
              }
              if (gSelected_day != -1 && gSelected_month != -1 && gSelected_year != -1) {
                if (gIs_iPhone) $setInnerText('sw_date_picked', format_date({m:gSelected_month, d:gSelected_day, y:gSelected_year}));
              }
              setupSelectElementByValue($('select_event'), gEvent_type_value);
              setupSelectElementByValue($('select_interval'), gUpdate_interval);
              setupSelectElementByValue($('select_date_format'), gDate_format);
              setupSelectElementByValue($('select_padding'), gPad);
              if (!gIs_iPhone) {
                setupSelectElementByText($('select_day'), gSelected_day);
                setupSelectElementByText($('select_month'), gSelected_month);
                setupSelectElementByText($('select_year'), gSelected_year);
              }
              if ($('title_input_field').value == "") Countdown.setTitleFieldResources(gEvent_type_text);
              if (gEvent_type_value == "other" || gEvent_type_value == "birthday") {
                $('title_input_field').disabled = false;
              } else if (gEvent_type_value == "midnight") {
                $('title_input_field').disabled = true;
              } else {
                $('title_input_field').disabled = true;
              }
          },
       
          doMain: function() {
              $setInnerText('pageTitle', $('title_input_field').value);
              var etv = $('select_event').options[$('select_event').selectedIndex].value;
              gTarget_time = setupDate(etv).getTime();
              gUpdate_date_mark = true;
              window.scrollTo(0, 1);
              if (gMy_interval_timer != null) {
                window.clearInterval(gMy_interval_timer);
              }
              gMy_interval_timer = window.setInterval('Countdown.countDown()', gUpdate_interval);
              window.setTimeout('Countdown.countDown()', 0); 
          },
       
          countDown: function() {
            var now, ms, diff, daysLeft, hrsLeft, minsLeft, secsLeft, today;
            now = new Date();
            now_day = now.getDate();
            now_month = now.getMonth();
            now_year = now.getFullYear();
            ms = now.getTime();
            diff = gTarget_time - ms;
            if (diff <= MSSEC && !gDone) { 
              daysLeft = hrsLeft = minsLeft = secsLeft = 0;
              if (gMy_interval_timer != null) {
                window.clearInterval(gMy_interval_timer);
                gMy_interval_timer = null;
              }
              $('counter_done').style.display = "block";
              $('settingsButton').style.display = "none";
              gDone = true;
            } else {
              daysLeft = Math.floor(diff / MSDAY);
              diff -= (daysLeft * MSDAY);
              hrsLeft = Math.floor(diff / MSHR);
              diff -= (hrsLeft * MSHR);
              minsLeft = Math.floor(diff / MSMIN);
              diff -= (minsLeft * MSMIN);
              secsLeft = Math.floor(diff / MSSEC);
              gDone = false;
            }
            setFields(daysLeft, hrsLeft, minsLeft, secsLeft);
            if (now_month != gMark_month || now_day != gMark_day || now_year != gMark_year || gUpdate_date_mark) {
              today = format_date({m:now_month+1, d:now_day, y:now_year});
              if ($getInnerText('today') != today) $setInnerText('today', today);
              gMark_month = now_month; gMark_day = now_day; gMark_year = now_year;
            }
          }, 
       
          setTitleFieldResources: function(et) {
            storeItem('title_input_field', et);
            gTitle_input_field = et;
            $('title_input_field').value = et;
          },
       
          updateEventType: function(obj) {
            var ett = obj.options[obj.selectedIndex].text;
            var etv = obj.options[obj.selectedIndex].value;
            setEventTypeResources(ett, etv); // sets gEvent_type and related fields
            if (etv == "other" || etv == "birthday") {
              Countdown.setTitleFieldResources(ett); // sets title_input_field and related fields
              $('title_input_field').disabled = false;
            } else if (ett == "Midnight" || ett == "New Years") {
              Countdown.setTitleFieldResources(ett);
              $('title_input_field').disabled = true;
            } else if (etv == "reset") {
              ett = "Midnight";
              etv = "midnight";
              alert("Clearing local storage & setting up default event 'Midnight'. Please reload Countdown app to see changes.");
              clearItems();
              Countdown.setTitleFieldResources(ett);
              setupSelectElementByValue($('select_event'), etv);
              setupSelectElementByValue($('select_interval'), "2000");
              setupSelectElementByValue($('select_date_format'), "mm/dd/yyyy");
              setupSelectElementByValue($('select_padding'), " ");
              Countdown.updateInterval($('select_interval'));
              Countdown.updateDateFormat($('select_date_format'));
              Countdown.updatePadding($('select_padding'));
              $('title_input_field').disabled = true;
           }
           gTarget_time = setupDate(etv).getTime();
           if (gIs_iPhone) $setInnerText('sw_date_picked', format_date({m:gSelected_month, d:gSelected_day, y:gSelected_year}));
           window.scrollTo(0, 1);
          }, 
       
          updateInterval: function(obj) {
            gUpdate_interval = parseInt(obj.options[obj.selectedIndex].value);
            $setInnerText('update_interval_text', obj.options[obj.selectedIndex].text);
            gUpdate_interval_text = obj.options[obj.selectedIndex].text;
            store_update_interval(gUpdate_interval, gUpdate_interval_text);
            window.clearInterval(gMy_interval_timer);
            gMy_interval_timer = window.setInterval('Countdown.countDown()', gUpdate_interval);
            window.scrollTo(0, 1);
          }, 
       
          updateSWDate: function() {
            var results = SpinningWheel.getSelectedValues(); 
            gSelected_year = results.keys[0];
            gSelected_month = results.keys[1];
            gSelected_day = results.keys[2];
            store_date(gSelected_year, gSelected_month, gSelected_day);
            $setInnerText('sw_date_picked', format_date({m:gSelected_month, d:gSelected_day, y:gSelected_year}));
            window.scrollTo(0, 1);
          },
       
          updateDateFormat: function(obj) {
            gDate_format = obj.options[obj.selectedIndex].value;
            storeItem('date_format', gDate_format);
            if (gIs_iPhone) $setInnerText('sw_date_picked', format_date({m:gSelected_month, d:gSelected_day, y:gSelected_year}));
            window.scrollTo(0, 1);
          },
       
          updateDay: function(obj) {
            gSelected_day = obj.options[obj.selectedIndex].text;
            storeItem('selected_day', gSelected_day);
          },
       
          updateMonth: function(obj) {
            gSelected_month = obj.options[obj.selectedIndex].text;
            storeItem('selected_month', gSelected_month);
          },
       
          updateYear: function(obj) {
            gSelected_year = obj.options[obj.selectedIndex].text;
            storeItem('selected_year', gSelected_year);
          },
       
          updatePadding: function(obj) {
            gPad = obj.options[obj.selectedIndex].value;
            storeItem('pad', gPad);
            window.scrollTo(0, 1);
          },
       
          turnActionButtonOn: function(btn, action) {
            actionButton(btn, action, "inline");
          },
       
          turnActionButtonOff: function(btn, action) {
            actionButton(btn, action, "none");
          },
       
          acknowledgeDone: function(obj) {
            obj.style.display='none';
            $('settingsButton').style.display='inline';
          }
        }
      }();
      /* END Countdown */

      Listing 3: Countdown Stylesheet (countdown.css)

      /*
        Copyright (c) 2009 Technetra Corp
        Released under The MIT License
      */
      body {
        background: #cccccc scroll repeat 0 0;
        font-family:sans-serif;
        font-size:16px;
      }
      body[orient="portrait"] {
        height:460px;
      }
      body[orient="landscape"] {
        height:300px;
      }
       
      h1 {
        font-size:1.5em;
        font-weight:bold;
      }
      #main_subtitles > span > span:first-child {
        color:#356AA0;
      }
      body[orient="portrait"] #main_subtitles {
        font-size:0.56em;
      }
      body[orient="landscape"] #main_subtitles {
        font-size:0.60em;
      }
      body[orient="landscape"] #main_subtitles > span > br {
        display:none;
      }
      .divider {
        color:red;
      }
      body[orient="portrait"] #main_subtitles > span:first-child .divider {
        display:none;
      }
       
      #main_footer {
        position:absolute;
        margin-left:10px;
      }
      body[orient="portrait"] #main_footer {
        top:350px;
      }
      body[orient="landscape"] #main_footer {
        top:210px;
      }
       
      #update_interval_container {
        font-size:0.8em;
        float:left;
        margin-top:-4px;
        color:#356AA0;
      }
      table {
        border-spacing:0;
        width:100%;
      }
      body[orient="landscape"] table {
        width:50%;
        float:left;
        border:none solid gray;
      }
       
      body[orient="portrait"] .overlay { width: 136px; }
      body[orient="landscape"] .overlay { width: 256px; }
       
      #seconds_overlay {
        position:relative;
        left:0;
        background-color:#3F4C6B;
        -khtml-opacity:0.5; -moz-opacity:0.5;
      }
      #minutes_overlay {
        position:relative;
        left:0;
        background-color:#CDEB8B;
        -khtml-opacity:0.5; -moz-opacity:0.5;
      }
      #hours_overlay {
        position:relative;
        left:0;
        background-color:#FFFF88;
        -khtml-opacity:0.5; -moz-opacity:0.5;
      }
      #days_overlay {
        position:relative;
        left:0;
        background-color:#B02B2C;
        -khtml-opacity:0.5; -moz-opacity:0.5;
      }
      #counter_done {
        position:absolute;
        min-height:66px;
        width:50%;
        text-align:center;
        -khtml-opacity:0.8; -moz-opacity:0.8;
      }
      body[orient="portrait"] #counter_done {
        top:200px;
        left:80px;
        font-size:1.0em;
      }
      body[orient="landscape"] #counter_done {
        top:130px;
        left:115px;
        font-size:1.2em;
      }
      .ctr {text-align:center}
       
      #countdown_box {
        width:100%;
        position:absolute;
      }
      body[orient="portrait"] #countdown_box {
        top:30px;
      }
      body[orient="landscape"] #countdown_box {
        top:40px;
      }
      .unit > div {
        position:absolute;
        text-align:right;
        font-weight:bold;
        padding-right:10px;
      }
      body[orient="portrait"] .unit > div {
        right:160px;
        height:58px;
      }
      body[orient="landscape"] .unit > div {
        right:204px;
        height:36px;
      }
      body[orient="portrait"] #days_box { top:60px; }
      body[orient="portrait"] #hours_box { top:118px; }
      body[orient="portrait"] #minutes_box { top:176px; }
      body[orient="portrait"] #seconds_box { top:234px; }
      body[orient="landscape"] #days_box { top:10px; }
      body[orient="landscape"] #hours_box { top:46px; }
      body[orient="landscape"] #minutes_box { top:82px; }
      body[orient="landscape"] #seconds_box { top:118px; }
       
      .unit_label {
        position:absolute;
        padding-left:10px;
        border-left:2px solid #ddd;
      }
      .unit span {
        padding-right:10px;
      }
      body[orient="portrait"] .unit span {
        font-size:2em;
        height:58px;
      }
      body[orient="landscape"] .unit span {
        font-size:1.5em;
        height:36px;
      }
       
      #days_box span[class="unit_label"] { color:#B02B2C; }
      #seconds_box span[class="unit_label"] { color:#3F4C6B; }
      #minutes_box span[class="unit_label"] { color:#73880A; }
      #hours_box span[class="unit_label"] { color:#C79810; }
       
      #sw-wrapper { display:block; }
      body[orient="portrait"] #sw-frame { bottom:112px; }
      body[orient="landscape"] #sw-frame { bottom:8px; }
       
      /* alignment correction for Shiretoko */
      .row > label {
        left:0;
      }
      div[class="row"] > input,select {
        margin-top:7px;
      }
      body[device="iphone"] #select_date_container { display:none; }
      body[device="unknown"] #spinning_wheel_date_container { display:none; }

© 2000-2009 Technetra. All rights reserved. Contact | Terms of Use

WordPress