mozilla
Your Search Results

    Creating the Area Tweet app

    With all of the design goals and coding standards laid out, it's time to get into the coding of our app.

    HTML

    As with any Web application template, we need an HTML page with the HTML5 doctype, <head>, and <body> elements.

    <!doctype html>
    <html>
    <head>
      <title></title>
     
      <meta charset="utf-8" />
      <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
     
      <meta name="viewport" content="width=device-width, initial-scale=1">
     
      <!-- stylesheets go here -->
     
    </head>
    <body>
     
      <!-- HTML app structure goes here -->
     
     
      <!-- javascript goes here -->
     
    </body>
    </html>
    

    We'll be using jQuery Mobile as the JavaScript framework. That means we must pull in the CSS and JavaScript resources required by jQuery Mobile:

    <!-- stylesheets -->
    <link rel="stylesheet" href="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.css" />
    	
    <!-- scripts -->
    <script src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
    <script src="http://code.jquery.com/mobile/1.1.0/jquery.mobile-1.1.0.min.js"></script>
    

    With our basic application boilerplate and jQuery Mobile resource in place, it's time to add content and widgets to the page. Since we've decided to use jQuery Mobile for our application framework, our app's HTML structure will follow their prescribed widget structures. The first pane provides a search box and a history list:

    <!-- home pane -->
    <div data-role="page" id="home">
     
      <div data-role="header">
        <h1>Area Tweet</h1>
      </div>
     
      <div data-role="content">
        
        <div class="ui-body ui-body-b">
          <h2>Location Search</h2>
          <form id="locationForm">
            <input type="search" name="location" id="locationInput" placeholder="Your Location" />
            <button type="submit" data-role="button" data-theme="b">Search</button>
          </form>
        </div>
        
        <div id="prevLocationsContainer" class="opaque">
          <h2>
            Previous Locations
            <a href="#" id="clearHistoryButton" data-role="button" data-icon="delete" data-iconpos="notext"
                    data-theme="b" data-inline="true" style="top: 5px;">Clear History</a>
          </h2>
          <ul id="prevLocationsList" data-role="listview" data-inset="true" data-filter="true"></ul>
        </div>
        
      </div>
    </div>
    

    There are a few things to notice with the code above:

    • The structure of the HTML5 sticks to semantics, which ensures maximum accessibility.
    • As with any Web-based app, we assign CSS classes and IDs to elements we want to style and to access to from JavaScript code.

    If you have questions about individual jQuery Mobile capabilities, node attributes, or structures, please consult the jQuery Mobile Documentation.

    The second pane will be used for displaying a list of tweets:

    <!-- tweets pane -->
    <div data-role="page" id="tweets">
     
      <div data-role="header">
        <a href="#home" id="tweetsBackButton">Back</a>
        
        <h1><span id="tweetsHeadTerm"></span> Tweets</h1>
      </div>
     
      <ul id="tweetsList" data-role="listview" data-inset="true"></ul>
     
    </div>
    

    CSS

    Our Web app's CSS contains a CSS animation for fading in any element, as well as for overriding jQuery Mobile styles and general element styling.

    /* animations */
    @-moz-keyframes fadeIn {
      0%    { opacity: 0; }
      100%  { opacity: 1; }
    }
    @-webkit-keyframes fadeIn {
      0%    { opacity: 0; }
      100%  { opacity: 1; }
    }
    
    .fadeIn {
      -moz-animation-name: fadeIn;
      -moz-animation-duration: 2s;
     
      -webkit-animation-name: fadeIn;
      -webkit-animation-duration: 2s;
     
      opacity: 1 !important;
    }
    
    /* Custom CSS classes */
    .clear        { clear: both; }
    .hidden       { display: none; }
    .opaque       { opacity: 0; }
    
    
    /* Basic styling */
    #prevLocationsContainer { margin-top: 40px; }
    
    /* Customizing jQuery Styles */
    #tweetsList li.ui-li-divider,
    #tweetsList li.ui-li-static     { font-weight: normal !important; }
    
    
    /* Tweet list */
    #tweetsList li {
     
    }
    
    #tweetsList li .tweetImage {
      float: left;
      margin: 10px 10px 0 10px;
    }
    
    #tweetsList li .tweetContent {
      float: left;
      /* margin: 10px 0 0 0; */
    }
    
    #tweetsList li .tweetContent strong {
      display: block;
      padding-bottom: 5px;
    }
    
    /* Media Queries for device support */
    @media screen and (max-device-width: 480px) {
      #prevLocationsContainer { margin-top: 10px; }
    }
    

    JavaScript

    JavaScript is the last major component to add to our Web app. The following encompasses all capabilities for the app:

    $(document).ready(function() {
     
      // Start from scratch on page load
      if(window.location.hash) {
        window.location = "index.html";
      }
     
      // Feature tests and settings
      var hasLocalStorage = "localStorage" in window,
        maxHistory = 10;
     
      // List of elements we'll use
      var $locationForm = $("#locationForm"),
        $locationInput = $("#locationInput"),
        
        $prevLocationsContainer = $("#prevLocationsContainer"),
        
        $tweetsHeadTerm = $("#tweetsHeadTerm"),
        $tweetsList = $("#tweetsList");
        
      // Hold last request jqXHR's so we can cancel to prevent multiple requests
      var lastRequest;
     
      // Create an application object
      app = {
        
        // App initialization
        init: function() {
          var self = this;
          
          // Focus on the search box
          focusOnLocationBox();
          
          // Add the form submission event
          $locationForm.on("submit", onFormSubmit);
          
          // Show history if there are items there
          this.history.init();
          
          // When the back button is clicked in the tweets pane, reset the form and focus
          $("#tweetsBackButton").on("click", function(e) {
            $locationInput.val("");
            setTimeout(focusOnLocationBox, 1000);
          });
          
          // Geolocate!
          geolocate();
          
          // When the tweets pane is swiped, go back to home
          $("#tweets").on("swiperight", function() {
            window.location.hash = "";
          });
          
          // Clear history when button clicked
          $("#clearHistoryButton").on("click", function(e) {
            e.preventDefault();
            localStorage.removeItem("history");
            self.history.hideList();
          })
        },
        
        // History modules
        history: {
          $listNode: $("#prevLocationsList"),
          $blockNode: $("#homePrev"),
          init: function() {
            var history = this.getItemsFromHistory(),
              self = this;
            
            // Add items to the list
            if(history.length) {
              history.forEach(function(item) {
                self.addItemToList(item);
              });
              self.showList();
            }
            
            // Use event delegation to look for list items clicked
            this.$listNode.delegate("a", "click", function(e) {
              $locationInput.val(e.target.textContent);
              onFormSubmit();
            });
          },
          getItemsFromHistory: function() {
            var history = "";
            
            if(hasLocalStorage) {
              history = localStorage.getItem("history");
            }
            
            return history ? JSON.parse(history) : [];
          },
          addItemToList: function(text, addToTop) {
            var $li = $("<li><a href='#'>" + text + "</a></li>"),
              listNode = this.$listNode[0];
              
            if(addToTop && listNode.childNodes.length) {
              $li.insertBefore(listNode.childNodes[0]);
            }
            else {
              $li.appendTo(this.$listNode);
            }
            
            this.$listNode.listview("refresh");
          },
          addItemToHistory: function(text, addListItem) {
            var currentItems = this.getItemsFromHistory(),
              newHistory = [text],
              self = this,
              found = false;
            
            // Cycle through the history, see if this is there
            $.each(currentItems, function(index, item) {
              if(item.toLowerCase() != text.toLowerCase()) {
                newHistory.push(item);
              }
              else {
                // We've hit a "repeater": signal to remove from list
                found = true;
                self.moveItemToTop(text);
              }
            });
            
            // Add a new item to the top of the list
            if(!found && addListItem) {
              this.addItemToList(text, true);
            }
            
            // Limit history to 10 items
            if(newHistory.length > maxHistory) {
              newHistory.length = maxHistory;
            }
            
            // Set new history
            if(hasLocalStorage) {
              // Wrap in try/catch block to prevent mobile safari issues with private browsing
              // http://frederictorres.blogspot.com/2011/11/quotaexceedederr-with-safari-mobile.html
              try {
                localStorage.setItem("history", JSON.stringify(newHistory));
              }
              catch(e){}
            }
            
            // Show the list
            this.showList();
          },
          showList: function() {
            $prevLocationsContainer.addClass("fadeIn");
            this.$listNode.listview("refresh");
          },
          hideList: function() {
            $prevLocationsContainer.removeClass("fadeIn");
          },
          moveItemToTop: function(text) {
            var self = this,
              $listNode = this.$listNode;
            
            $listNode.children().each(function() {
              if($.trim(this.textContent.toLowerCase()) == text.toLowerCase()) {
                $listNode[0].removeChild(this);
                self.addItemToList(text, true);
              }
            });
            
            $listNode.listview("refresh");
          }
        }
        
      };
     
      // Search submission
      function onFormSubmit(e) {
        if(e) e.preventDefault();
        
        // Trim the value
        var value = $.trim($locationInput.val());
        
        // Move to the tweets pane
        if(value) {
          
          // Add the search to history
          app.history.addItemToHistory(value, true);
          
          // Update the pane 2 header
          $tweetsHeadTerm.html(value);
          
          // If there's another request at the moment, cancel it
          if(lastRequest && lastRequest.readyState != 4) {
            lastRequest.abort();
          }
          
          // Make the JSONP call to Twitter
          lastRequest = $.ajax("http://search.twitter.com/search.json", {
            cache: false,
            crossDomain: true,
            data: {
              q: value
            },
            dataType: "jsonp",
            jsonpCallback: "twitterCallback",
            timeout: 3000
          });
        }
        else {
          // Focus on the search box
          focusOnLocationBox();
        }
        
        return false;
      }
     
      // Twitter reception
      window.twitterCallback = function(json) {
        
        var template = "<li><img src='{profile_image_url}' class='tweetImage' /><div class='tweetContent'>"
                       +"<strong>{from_user}</strong>{text}</div><div class='clear'></div></li>",
          tweetHTMLs = [];
          
        // Basic error handling
        if(json.error) { // Error for twitter
          showDialog("Twitter Error", "Twitter cannot provide tweet data.");
          return;
        }
        else if(!json.results.length) { // No results
          showDialog("Twitter Error", "No tweets could be found in your area.");
          return;
        }
        
        // Format the tweets
        $.each(json.results, function(index, item) {
          item.text = item.text.
                replace(/(https?:\/\/\S+)/gi,'<a href="$1" target="_blank">$1</a>').
                replace(/(^|\s)@(\w+)/g,'$1<a href="http://twitter.com/$2" target="_blank">@$2</a>').
                replace(/(^|\s)#(\w+)/g,'$1<a href="http://search.twitter.com/search?q=%23$2" target="_blank">#$2</a>')
          tweetHTMLs.push(substitute(template, item));
        });
        
        // Place tweet data into the form
        $tweetsList.html(tweetHTMLs.join(""));
        
        // Refresh the list view for proper formatting
        try {
          $tweetsList.listview("refresh");
        }
        catch(e) {}
        
        // Go to the tweets view
        window.location.hash = "tweets";
      };
     
      // Template substitution
      function substitute(str, obj) {
        return str.replace((/\\?{([^{}]+)}/g), function(match, name){
          if (match.charAt(0) == '\\') return match.slice(1);
          return (obj[name] != null) ? obj[name] : "";
        });
      }
     
      // Geolocates the user
      function geolocate() {
        if("geolocation" in navigator) {
          // Attempt to get the user position
          navigator.geolocation.getCurrentPosition(function(position) {
            // Set the address position
            if(position.address && position.address.city) {
              $locationInput.val(position.address.city);
            }
          });
        }
      }
     
      // Focuses on the input box
      function focusOnLocationBox() {
        $locationInput[0].focus();
      }
     
      // Modal function
      function showDialog(title, message) {
        $("#errorDialog h2.error-title").html(title);
        $("#errorDialog p.error-message").html(message);
        $.mobile.changePage("#errorDialog");
      }
     
      // Initialize the app
      app.init();
     
    });
    

    We won't go over every part of the app's JavaScript, but a few items are worth pointing out:

    • The script uses feature testing for both localStorage and geolocation. The app will not attempt to use either feature if they aren't present in the browser.
    • The geolocation API is used to detect the user's current location and the localStorage API is used to keep the user's search history.
    • The JavaScript has been written in a modular manner for easier testing.
    • A swipe event is listened for on the tweet pane. When the pane is swiped, the user is taken back to the first pane.
    • Twitter is the source of all data. No custom server-side processing is used for this app.

     

    Document Tags and Contributors

    Tags: 
    Contributors to this page: Sheppy, kohei.yoshino, Machado, rsage, markg, Selinger
    Last updated by: rsage,