When Mobile Browsers

Attack!





mobile-browsers-attack.appspot.com
@pamelafox


(use the space bar)

Browsers vs. Happiness

The Maps API Years



The Wave Years


My Web App


..Plus Mobile App


PhoneGap + Zepto + Bootstrap



Mobile Browsers



will make you cry.

Fixed Positions



function setupFixedFix() {
    if (isFixedBroken()) {
      $('.container-fluid').css({'position': 'static'});
      $('.actionsbar, .navbar').css({'position': 'absolute'});
      fixFixed();
      $(window).bind('scroll', fixFixed);
      $('.mobile-page').bind('touchend', fixFixed);
    }
}

function isFixedBroken(){
    // Feature detection didnt seem to work so we check the user agent instead
    return (ED.util.isIOS() && navigator.userAgent.match(/[5-9]_[0-9]/) === null);
}

function fixFixed() {
    if (isFixedBroken()) {
      $('.actionsbar').css({'top': (window.pageYOffset) + 'px'});
      $('.navbar').css({'top': (window.pageYOffset + window.innerHeight - 30) + 'px'});
    }
}

Touch Events



ED.util.addTouchOrClickHandler($('.mobile-log-a'), openPageLink);
				    
ED.util.addClickHandler($('.mobile-footer-a'), openPageLink);
ED.util.addClickHandler($('.mobile-stream-a'), openStreamLink);
function addTouchOrClickHandler(dom, callback, logThis) {
    if (useTouchEvents()) {
      dom.each(function() {
        $(this).unbind('tap', callback);
        $(this).bind('tap', callback);

        $(this).bind('touchstart', function(e) {
          e.preventDefault();
          var item = e.currentTarget;
          if (ISTOUCHING) return;
          item.moved = false;
          ISTOUCHING = true;
          item.startX = e.touches[0].pageX;
          item.startY = e.touches[0].pageY;
          $(item).addClass('active');
        });

        $(this).bind('touchmove', function(e) {
          var item = e.currentTarget;
          if (Math.abs(e.touches[0].pageX - item.startX) > 10 ||
              Math.abs(e.touches[0].pageY - item.startY) > 10) {
            item.moved = true;
            $(item).removeClass('active');
          }
        });

        $(this).bind('touchend', function(e) {
          var item = e.currentTarget;
          ISTOUCHING = false;
          if(!item.moved) {
            $(item).trigger('tap');
          }
          setTimeout(function() {
            $(item).removeClass('active');
          }, 300);
          delete item.moved;
        });

      });
    } else {
      dom.unbind('click', callback).bind('click', callback);
    }
  }
				    
def click_button(self, selector):
        if self.driver.name == 'iPhone':
            self.driver.execute_script('$("%s").trigger("tap")' % (selector))
        else:
            try:
                self.get_el(selector).click()

Scrolling


function show() {
    ED.util.resetScroll();
    ED.util.makeAutoResize($(DOM.notesInput));
    ED.util.putCursorAtEnd($(DOM.notesInput));
}

function resetScroll(top) {
    top = top || 0;
    $(document).scrollTop(top);
    window.setTimeout(function() {
      $(document).scrollTop(top);
    }, 10);
  }

Scrolling+Fixed+Touch

☞ Device Bugs: #1

Keyboards

<input type="number" step="0.01">

☞ Triggering Numeric Keyboards

Hiding the Keyboard

function hideKeyboard() {
    $('input, textarea').blur();

    if (ED.util.isIOS()) {
      document.activeElement.blur();
    }
    if (ED.util.isAndroid()) {
      var field = $('<input type="text">');
      $('#mobile-logs-page').append(field);

      setTimeout(function() {
          field.focus();
          setTimeout(function() {
              field.hide();
          }, 50);
      }, 50);
    }
  }

FOCUS!

/**
 * This is a hack to make text input focus work in Android/PG
 * where calling el.focus() doesn't actually have the 
 * blinking cursor effect = scumbag.
 * @param {Zepto} $el A zepto element.
 */
ws.focus = function($el) {
  var el = $el.get(0);
  el.focus();
  el.setSelectionRange && el.setSelectionRange(0, 0);
};

Courtesy @elsigh

Canvas


canvasContext.drawImage(img, x, y, width, height);
var dataUrl = canvas.toDataURL('image/png');
if (dataUrl.indexOf('data:image/png') == -1) {
  onFail('Got back an invalid data URL');
} else {
  onSuccess(dataUrl.replace('data:image/png;base64,', ''));
}
                
var cachedSupportsDataURL;
  function supportsDataURL() {
    if (cachedSupportsDataURL === undefined) {
      var c = document.createElement('canvas');
      var dataUrl = c.toDataURL('image/png');
      cachedSupportsDataURL = (dataUrl.indexOf('data:image/png') === 0);
    }
    return cachedSupportsDataURL;
  }             

☞ Issue 7901: Canvas.toDataURL() implementation

Offline Detection

var MSG_LOG_OFFLINE = 'We can\'t connect to the network now, so your log may be out of date.';
runIfOnline(function() {
    fetchNewData();
    }, MSG_LOG_OFFLINE);
}
function runIfOnline(onlineFunc, message, offlineFunc) {

    message = message || 'Sorry, we can\'t connect to the network right now.';
    if (isOnline()) {
      try {
        onlineFunc();
      }  catch(e) {
        logError(e);
      }
    } else {
      alert(message);
      if (offlineFunc) {
        try {
          offlineFunc();
        } catch(ee) {
          logError(ee);
        }
      }
    }
}
function isOnline() {
    // Presume online unless PhoneGap or HTML5 tell us otherwise.
    if (navigator.network && navigator.network.connection.type == Connection.NONE && 0) {
      ED.util.log('Found Connection.NONE');
      return false;
    }
    if (navigator.onLine === false) {
      ED.util.log('Found navigator.onLine is false');
      return false;
    }
    return true;
}

Loading Performance

☞ Measuring PhoneGap Loading Performance


  var timedEvents = [];
  function timeEvent(name) {
    timedEvents.push({'name': name || 'unnamed', time: Date.now()});
  }

  function showTimedEvents() {
    var timeText = '';
    var timeHtml = '<table>';
    for (var i = 0; i < timedEvents.length; i++) {
      var timedEvent = timedEvents[i];
      timeText += timedEvent.name + ': ' + timedEvent.time;
      var diff = '';
      if (i > 0) {
        diff = (timedEvent.time - timedEvents[i-1].time);
        timeText += ' (' + diff + 'ms elapsed)';
      }
      timeHtml += '<tr><td>' + timedEvent.name + '<td>' + timedEvent.time + '<td>' + diff;
      timeText += '\n';
    }
    console.log(timeText);
    document.body.innerHTML += timeHtml;
  }
              
<html>
 <head>  
 <script>
  // Timer functions
  </script>
  <script>timeEvent('Before CSS');</script>
  <link rel="stylesheet" href="css/all-phonegap-min.css?v=10201550"/>
  <script>timeEvent('After CSS');</script>
 </head>
 <body>
  <div>HTML here (more than this)</div>
<script>timeEvent('After HTML');</script>
<script src="js/libs/jquery-1.6.2.min.js"></script>
<script>timeEvent('After jQuery script tag');</script>
<script src="js/all-phonegap-min.js?v=10201550"></script>
<script>timeEvent('After other script tag');</script>
<script>

$(document).ready(function() {
  timeEvent('After document ready');
  myCustomMobileFunction();
  timeEvent('After ready JS called');
  showTimedEvents();
});
</script>
</body>
</html>

jQuery -> Zepto

Shaved: 22%

☞ Porting from jQuery to Zepto

onload -> deviceready

Shaved: 67%

document.addEventListener("DOMContentLoaded", function() {
    ED.mobile.setupMobile();
  });
document.addEventListener('deviceready', function() {
  ED.mobile.setupPhonegap();
}, false);
  

function setupMobile() {
    // Sets up the page CSS
    // Binds events to DOM
}
                    
function setupPhonegap() {
    // Shows splash page
    // Fetches data
    // Binds PhoneGap events
}
☞ Get to deviceready faster on Android

Rendering Performance

☞ Working Around Android Webkit
.android {
  .modal {
    @include box-shadow(none);
    @include background-clip(border-box);
    @include border-radius(0px);
    border: 1px solid black;
  }
}
$(window).on('scroll', $.throttle(500, loadVisibleImages));
☞ Delayed Image Loading on Long Pages
$textAreas.autoResizer({resizeOnChange: !isAndroid()});
window.setTimeout(function() {
    ED.shared.setupStream();
    ED.shared.setupStreamTimer();
}, 2000);

window.setTimeout(function() {
    ED.shared.maybeFetchStream();
}, 100);
    
window.setTimeout(function() {
    fetchNewData();
}, 1000); 
☞ Zakas: Responsive Interfaces



The future is bright!

Better Browsers

☞ Android Version Dashboards

☞ iOS version stats

☞ Wikimedia Traffic Analysis

MUCH Better Browsers

☞ Issue 113088: Let Chrome be used by WebView

☞ Firefox OS

Better Tools

☞ Adobe Edge Inspect
☞ HTML Mobile Debuggers Manifesto

Better Docs

☞ The Great Webkit Comparison

☞ Mobile HTML5, ☞ caniuse.com ☞ Device Bugs Tracker

Growing Community



...that's you!