PhoneGap by Dissection
My first PhoneGap 3.x app
Daniel Rhodes
This book is for sale at http://leanpub.com/phonegapbydissection
This version was published on 2015-02-26
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools
and many iterations to get reader feedback, pivot until you have the right book and build
traction once you do.
©2015 Daniel Rhodes
Dedicated to all the hard-working girls and boys in the free and open source software
communities.
Contents
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1 Conventions used in the text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
2. What you’ll need . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
3. What is PhoneGap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
4. Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
4.1 The cool new way . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
4.2 The fiddly older way . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5. Quick run-through of the default app . . . . . . . . . . . . . . . . . . . . . . . . . . 9
6. First things first: The layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
7. First things first: The tabbing mechanism . . . . . . . . . . . . . . . . . . . . . . . . 27
8. The Search tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
8.1 Layout and interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
8.2 Creating the database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
8.3 Querying the database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
8.4 Results scrolling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
8.5 Extra credit challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
9. The Discover tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
9.1 Layout and interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
9.2 Extra credit challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
10.The Write tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
10.1 Layout and interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
10.2 Filling the screen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
10.3 Displaying a random character . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
10.4 Finger doodling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
10.5 Extra credit challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
11.Splash screen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
12.Launcher icon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
13.Submitting to Google Play . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
CONTENTS
14.That’s all folks! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
1. Introduction
This book is going to teach you how to get started with mobile app development using the
PhoneGap platform. We’ll essentially rebuild, from scratch, a basic yet fully-functional app that
really exists! It’s called Japxlate and can be found here in the Google Play Store. The app is a
Japanese dictionary that you can search - even if offline. Not to worry though, we won’t get
bogged down in the nitty gritty of Japanese linguistics. We’ll focus on setting up, building and
finally deploying the app. You’ll laugh, you’ll cry, you’ll sick a little bit in the back of your throat,
but the journey will definitely be worth it…
This is version 1.0 of the book, first published February 2015 (v0.9 first published
January 2014)
Latest source code for the app is at https://github.com/danielrhodeswarp/japxlate-
android
This book was written using PhoneGap v3.1.0, but has been updated to cover anything
new or different in v3.3.0
1.1 Conventions used in the text
A command that you need to type on the Linux command line will look like:
you@yours$ somewhere]$ some linux command to type
Code (of any type - CSS, HTML or JavaScript) that you need to type in will look like:
//does the cursor have random fractals?
function checkRandomFractals()
{
return something.or.other;
}
HTML elements will be referred to like:
<elementname>
Code fragments, variable names, method names etc will look like:
Introduction 2
someMethod();
File names and folder names will look like:
/assets/www/some_file.html
A side note, something tangental to the main text, will look like:
..
I’m hungry but my teeth hurt.
New or updated information relevant for PhoneGap v3.3.0 will look like:
PhoneGap v3.3.0 uses the “Plugman” plugin manager.
2. What you’ll need
To keep things small and simple we’ll focus solely on developing on Linux for an app that we’ll
make for Android. Though one huge benefit of PhoneGap is that you can package the same(ish)
code into a working app for many different mobile platforms. We also won’t be using any third-
party JavaScript or CSS libraries, though these will be useful to you going forward with your
app development. What you’ll need:
• A Linux desktop box
• PhoneGap (which requires NodeJS) on the above box - at time of writing this tutorial I
was using version 3.1.0. Don’t worry, we’ll install this in the Getting started chapter
• As many Android devices as you can get your hands on! At least one
• Google’s “Android Developer Tools” bundle - or at least Eclipse with the Android plugins.
Again we’ll cover this in the Getting started chapter
• At least a lower-intermediate knowledge of HTML5, JavaScript and CSS
• To not be terrified of the Linux command line!
3. What is PhoneGap
PhoneGap is a way to make apps for mobile devices using standard website frontend technolo-
gies. Namely HTML5, JavaScript and CSS. PhoneGap is free and open source. PhoneGap apps
aren’t true or native apps, but rather they are apps that open up a “WebView” on you mobile
device - essentially a web browser in fullscreen mode without title bars or bezels - running your
frontend code. It’s not a million miles away from a desktop browser running in fullscreen mode
(usually accessed by pressing F11). Implemented well, this non-nativeness isn’t necessarily a bad
thing.
..
PhoneGap versus Cordova
You’ve probably come across the term “Cordova” in your research for PhoneGap. PhoneGap
and Cordova are very closely related, and so it’s worth explaining the difference. There’s a lot
of back-story here which I’ll skip, but in a nutshell:
PhoneGap is a software product by Adobe Systems Inc. It is a branded and maintained
distribution of:
Cordova, which is a free and open source project maintained by the Apache Software
Foundation (ASF).
At the time of writing, PhoneGap adds a cloud build service to basic Cordova. This changes
the command line for PhoneGap (versus Cordova) somewhat, though you should be able to -
in theory - follow this tutorial using plain vanilla Cordova instead of PhoneGap. I also noticed,
annoyingly, that a lot of PhoneGap documentation simply points to Cordova documentation
which can mean that the command line syntax is wrong.
4. Getting started
There are two routes we can go down to get started with PhoneGap development. Both routes
require the Android SDK to be installed so let’s do that first. The easiest way to install the Android
SDK is to install the Android Developer Tools (or ADT) bundle. This bundle installs the Android
SDK and Eclipse IDE configured for Android (native) development.
Right, let’s install the Android Developer Tools. The easy peasy way is to download and install
the “ADT Bundle for Linux” from http://developer.android.com/sdk/index.html which should be
worry free.
If you’re already using Eclipse IDE, you can simply download the Android Developer Tools
plugin for it at http://developer.android.com/tools/index.html
..
About IDEs
You aren’t forced to use Eclipse IDE for Android development, though it does make a lot of
things easier as it supports direct deploy to an actual Android device and it has a virtual device
manager for deploying to emulated Android devices.
Myself, I didn’t like the way that Eclipse was opening - and highlighting - the various
frontend source files for the app (though I don’t doubt that this is configurable in the options
somewhere!). There’s also the fact that it doesn’t speak PhoneGap. I found myself cutting the
code in NetBeans IDE and checking in with Eclipse every now and again to deploy to the actual
device (Ctrl-F11) or to check console.log() messages in LogCat.
Netbeans IDE v7.4 dropped just before I finished this tutorial and interestingly that seems to
have PhoneGap (well, Cordova) support built in! Definitely worth a look.
Bizarrely, I found that regardless of the IDE used, I often had to deploy to the device twice
in order to have it truly updated. This happened whenever a resource file was updated, ie.
JavaScript or HTML or CSS. I notice this doesn’t happen when Java sources are edited which
indicates some kind of caching issue. I still haven’t got to the bottom of this particular mystery.
4.1 The cool new way
OK, now we can install PhoneGap itself. For some strange reason that I can’t figure out (I’m
guessing it’s just for package management) it requires NodeJS so go to http://nodejs.org and
install it. Then, as we see at http://phonegap.com/install we simply do (on the command line):
you@yours$ somewhere]$ sudo npm install -g phonegap
Getting started 6
This installs the PhoneGap binaries and commands globally on our system. After that, let’s
actually create the PhoneGap project where we’ll put all of our lovely code for the app. There
are two slightly different syntaxes for this:
you@yours$ somewhere]$ phonegap create --name "Japxlate" --id "com.drappenheimer.japxla
te" japxlate
or
you@yours$ somewhere]$ phonegap create japxlate com.drappenheimer.japxlate "Japxlate"
This will create a PhoneGap project folder structure for building the same code to many
different device targets (Android or iOS etc). "Japxlate" is the name of our app (in quotes).
com.drappenheimer.japxlate is our app’s reverse domain name identifier. All Android apps
have a unique identifier like this. japxlate is our desired folder name for the project. We then
want to do:
you@yours$ somewhere]$ cd japxlate
you@yours$ japxlate]$ phonegap run android
Which will detect your Android SDK and try to run the app on the currently connected device
(or configured virtual machine). If no Android SDK is found or present, it will try to deploy the
app to your account on the PhoneGap remote cloud build environment - which is just out of
beta at time of writing. But you’ll more than likely need an extra bit of setup to get this run
android command to work. Specifically you’ll need to add a couple of folders from the Android
SDK install to your PATH. The gory details are at http://docs.phonegap.com/en/edge/guide_-
platforms_android_index.md.html#Android%20Platform%20Guide, but how I did it was to add
the following lines to my ∼/.bashrc file:
export ANDROID_SDK_HOME=/wherever/you/installed/it/adt-bundle-linux-x86_64-20130729/sdk
export PATH=${PATH}:${ANDROID_SDK_HOME}/platform-tools:${ANDROID_SDK_HOME}/tools
As well as this I personally needed the Java development libraries to be installed.
If the run android command still doesn’t work after all this configuration, double check your
Android SDK Manager which you can reach from the Eclipse IDE.
Note that this run command is a shortcut for the build followed by install commands. If you
don’t want to actually run your PhoneGap app from the command line, you need to at least build
it which is like this:
you@yours$ japxlate]$ phonegap build android
This will create a PROJECTROOT/platforms/android folder with skeleton source files for our app
in it. And importantly the project files for this to be pickupable as an Android project in Eclipse
IDE.
Getting started 7
..
How many mobile platforms does it take to
change a lightbulb?
You might be wondering now, if PhoneGap is supposed to be this amazing tool that lets us
write the same app code for multiple mobile platforms, why would we want to dive straight in
to the /platforms/android folder? How is that going to work on, say, iOS?
The answer is simple, PhoneGap is indeed a tool where the same app code can be compiled
for multiple mobile platforms, but - in a nutshell - we are cheating and taking a shortcut! This
tutorial is rather simplified and focuses solely on Android. This is why we dive right in at
/platforms/android.
If your app needs to work on multiple mobile platforms - as most apps do - then you should
really create your app’s code in PROJECTROOT/www, specifying any platform-specific customisa-
tions in PROJECTROOT/merges, then debug each time for your platforms with the build, install
and run commands. The excellent blog post at http://devgirl.org/2013/09/05/phonegap-3-0-
stuff-you-should-know/ explains this very well.
Like the run command, the build command will also fallback to the remote cloud build
environment. You can disable this fallback with the command phonegap local build android.
Right, so now you’ve at least built your app on the command line. You might even have run it
from the command line! Going forward with this tutorial, let’s plug the skeleton code we’ve just
built into our Eclipse IDE as an Android project. Follow these steps:
1. Click File ⇒ New ⇒ Project
2. Select Android ⇒ Android Project from Existing Code (note there’s also a sample native
project in there!)
3. Browse to PROJECTROOT/platforms/android folder (actually just PROJECTROOT seems to
also work)
4. Click OK
5. You’ll get an “Import Projects” dialogue now with the project details that you can confirm
/ change and then click Finish
..
Keeping your PhoneGap up-to-date
Installing PhoneGap via NodeJS has the nice advantage that you can keep your PhoneGap
version up-to-date by running this command:
you@yours$ somewhere]$ sudo npm update -g phonegap
Getting started 8
4.2 The fiddly older way
An older way of getting started (that PhoneGap up to v2.1.0 used) still works and can be useful
if you are struggling with the configuration steps details in the above section. You’ll still need
to have Eclipse with the ADT installed first, but you won’t have to fiddle around with installing
NodeJS or altering PATH environment variables.
Simply download - rather than install - the relevant “archive” version of PhoneGap from
http://phonegap.com/install, and then you can follow the steps from “Setup New Project” in
the PhoneGap documentation. Please note that these instructions are for older versions of
PhoneGap and Eclipse and so your mileage with the latest versions may vary.
This page on the Adobe website is also a useful reference.
Sorry but I can’t specify exactly how to do it this way as it is not the supported way any more.
It might stop working for future versions of PhoneGap. Though I could get it working - with a
few tweaks - with PhoneGap v3.1.0.
Advantages of this method: You don’t have to install PhoneGap or NodeJS or any dependencies.
Disadvantages of this method: You don’t get PhoneGap’s latest template for setting up an Android
app and you have to do it manually (ie. updating the manifest etc).
5. Quick run-through of the default
app
Our app starts life as the PhoneGap “Hello world” app (unless you went The fiddly older way in
which case it’s empty). This is a good starting point and has some things we can build on and
learn from. Of course we’ll need to ditch a lot of it as well!
Go ahead, hit CTRL-F11 in Eclipse to run the app on your virtual or actual device. We get a little
robot icon and a pulsing (via CSS3) “device is ready” message. Rotate your device, it redraws
itself accordingly and changes the layout slightly if needed. It also doesn’t present or allow any
kind of scrolling or pinching which is A Good Thing for most apps - including Japxlate.
Figure 1. The default PhoneGap app (landscape)
The files that we’ll be wanting to edit (CSS, HTML5, JavaScript) to make our own app can be
found in the assets/www folder of our Eclipse project.
Let’s take a look at the generated assets/www/index.html (Apache licence text removed for
brevity):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale
=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device
-dpi" />
<link rel="stylesheet" type="text/css" href="css/index.css" />
<title>Hello World</title>
</head>
<body>
Quick run-through of the default app 10
<div class="app">
<h1>PhoneGap</h1>
<div id="deviceready" class="blink">
<p class="event listening">Connecting to Device</p>
<p class="event received">Device is Ready</p>
</div>
</div>
<script type="text/javascript" src="phonegap.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript">
app.initialize();
</script>
</body>
</html>
PhoneGap v3.3.0 adds a comment talking about a workaround for iOS 7.
We’ve got the simplified “html” DOCTYPE for HTML5. We explicity set a charset of utf-8
Unicode which is clearly going to be very important for this app! We’ve got a lot of “viewport”
settings which are mostly self-explanatory, but essentially say “this app fills the device display,
defaults to 100% zoom and can not be zoomed in or out”. This is really going to help our PhoneGap
app look and feel more like a native app and not a web browser view.
We then link to some CSS which we’ll look at shortly. The <title> needs updating, but this
won’t normally be visible to the app user anyway. Especially as PhoneGap build puts a theme
setting of Theme.Black.NoTitleBar in AndroidManifest.xml.
Then the <body> starts and we have whatever markup the app needs. Just before the <body>
closes, we have links to some JavaScript (this is debated but considered to be something of a
performance improvement). phonegap.js (in assets/www) is the PhoneGap library and is how
we can access phone hardware (ie. camera) from JavaScript in our PhoneGap app. Commenting
out this file will enable you to somewhat preview the app just by opening the index.html file in
Chrome desktop browser. We’ll talk about this later.
js/index.js is JavaScript specifically for this app. We then call app.initialize(). The app
object is in index.js which we’ll look at after taking a quick peek at the key things in the CSS
file we mentioned a moment ago (Apache licence text removed for brevity):
Quick run-through of the default app 11
* {
-webkit-tap-highlight-color: rgba(0,0,0,0); /* make transparent link selection, adj
ust last value opacity 0 to 1.0 */
}
body {
-webkit-touch-callout: none; /* prevent callout to copy image, etc w
hen tap to hold */
-webkit-text-size-adjust: none; /* prevent webkit from resizing text to
fit */
-webkit-user-select: none; /* prevent copy paste, to allow, change
'none' to 'text' */
background-color:#E4E4E4;
background-image:linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%);
background-image:-webkit-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%);
background-image:-ms-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%);
background-image:-webkit-gradient(
linear,
left top,
left bottom,
color-stop(0, #A7A7A7),
color-stop(0.51, #E4E4E4)
);
background-attachment:fixed;
font-family:'HelveticaNeue-Light', 'HelveticaNeue', Helvetica, Arial, sans-serif;
font-size:12px;
height:100%;
margin:0px;
padding:0px;
text-transform:uppercase;
width:100%;
}
/* Portrait layout (default) */
.app {
background:url(../img/logo.png) no-repeat center top; /* 170px x 200px */
position:absolute; /* position in the center of the screen */
left:50%;
top:50%;
height:50px; /* text area height */
width:225px; /* text area width */
text-align:center;
padding:180px 0px 0px 0px; /* image height is 200px (bottom 20px are overlapped
with text) */
margin:-115px 0px 0px -112px; /* offset vertical: half of image height and text ar
ea height */
/* offset horizontal: half of text area width */
}
Quick run-through of the default app 12
/* Landscape layout (with min-width) */
@media screen and (min-aspect-ratio: 1/1) and (min-width:400px) {
.app {
background-position:left center;
padding:75px 0px 75px 170px; /* padding-top + padding-bottom + text area = ima
ge height */
margin:-90px 0px 0px -198px; /* offset vertical: half of image height */
/* offset horizontal: half of image width and tex
t area width */
}
}
.
.
The clause for * simply removes, from any element that we might make tappable, the default
sickly orange highlight that Android WebView gives to links and buttons and things.
The body clause starts by disabling some default Android WebView interations. This makes our
PhoneGap app feel a bit more nativey.
Then we set a grey gradient as the background.
Then we set the font type and size (12px). Height and width are both set to 100% which makes
our <body> fill the size of the WebView screen. We specify no margin (which is gap space outside
the <body>) and no padding (which is gap space inside the <body>).
In .app - our top level div in the markup - we set the layout of our app specific things. Portrait
orientation is assumed - a safe assumption for most phone apps. I won’t bore you with this too
much (but if you are baffled then please see a CSS refresher) other than to say it pulls some
strings with absolute positioning and negative margins to centre a background image and some
text.
Then we have another .app block wrapped in what’s called a media query
(http://cssmediaqueries.com/what-are-css-media-queries.html is a useful introduction) which
triggers when the phone is rotated into landscape view. It moves the background image to the
left of the text and also moves the text such that things are still centred.
Right, let’s get back to that js/index.js file that we’ve almost forgotten about! (Apache licence
text removed for brevity):
var app = {
// Application Constructor
initialize: function() {
this.bindEvents();
},
// Bind Event Listeners
//
// Bind any events that are required on startup. Common events are:
// 'load', 'deviceready', 'offline', and 'online'.
bindEvents: function() {
document.addEventListener('deviceready', this.onDeviceReady, false);
Quick run-through of the default app 13
},
// deviceready Event Handler
//
// The scope of 'this' is the event. In order to call the 'receivedEvent'
// function, we must explicity call 'app.receivedEvent(...);'
onDeviceReady: function() {
app.receivedEvent('deviceready');
},
// Update DOM on a Received Event
receivedEvent: function(id) {
var parentElement = document.getElementById(id);
var listeningElement = parentElement.querySelector('.listening');
var receivedElement = parentElement.querySelector('.received');
listeningElement.setAttribute('style', 'display:none;');
receivedElement.setAttribute('style', 'display:block;');
console.log('Received Event: ' + id);
}
};
All we have is one object called app which represents - wait for it! - our PhoneGap app.
initialize() is the constructor. We call this directly from index.html if you remember.
initialize() simply calls app.bindEvents() which in turn uses a DOM standard way of adding
an event listener. The event we listen for here is ‘deviceready’ which is fired from the PhoneGap
library when our Android device is, well, ready. We specify that this event is to be handled by
app.onDeviceReady() which simply calls app.receivedEvent('deviceready').
app.receivedEvent('deviceready') simply hides the “connecting” message and displays the
“ready” message (which are displayed and hidden, respectively, via the default index.css).
someElement.querySelector() is very interesting here and we’ll look at that later.
console.log(someMessage) is worth talking about now because we are going to be hammering it
during development! Basically this logs something to the browser’s console without disturbing
the user. When running your app via Eclipse’s F11, console.log() messages that fire on the
device will show up in your Eclipse’s “LogCat” thus:
Quick run-through of the default app 14
Figure 2. console.log() messages as appearing in Eclipse’s LogCat
Or, if debugging in Chrome desktop, you can see it by pressing F12 on the page in question then
clicking the console tab:
Figure 3. console.log() messages as appearing in Chrome desktop’s debugger
console.log() (and there are actually some other methods) is a general JavaScript development
technique that isn’t specific to mobile development. It works on all major browsers (though IE
needs help!).
6. First things first: The layout
Japxlate is going to have a single screen or “intent”. It won’t jump out to, for example, your
phone’s camera intent or “share to” list. The single screen is going to have three tab options -
Search, Discover and Write. We want the tab navigation and current tab content to all fit on the
device display without scrolling. OK, the PhoneGap Hello World app we just looked at is a good
start, but let’s see what tweaks we can do.
The Japxlate app is a spinoff of the @japxlate Twitter channel, so let’s look at that to get some
design ideas:
Figure 4. The @japxlate Twitter channel
OK, so we’ve got a greyish background. The logo is a red ‘J’ on a white background. The red
is our signature red and is actually #990000. The red ‘J’ on a white background is going to be a
good launcher icon for our app which we’ll talk about in a later chapter.
Right, so we need three tabs and we have some colour ideas. Here’s a quick wireframe:
First things first: The layout 16
Figure 5. Quick wireframe of the Japxlate app layout
Let’s put our tabs at the top so they’re out of the way of our device’s core Android buttons (back,
home, menu / special). Let’s have a little footer and see if we need that. The footer and header
have grey backgrounds. The tab content area is bog-standard black text on a white background.
When a tab is tapped, the header and footer will stay the same (though possibly with some kind
of current tab highlight) but the content area will load the appropriate content for that tab.
HTML5 gives us <header> and <footer> elements, so let’s try those. Change the <body> in
index.html to look like:
<body>
<header>
header
</header>
<div class="japxlate_app"> <!--note we've changed the class name-->
content area
</div>
<footer>
footer
</footer>
<!--<script type="text/javascript" src="phonegap.js"></script>-->
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript">
app.initialize();
</script>
</body>
Fire this up on your device (or desktop Chrome) and it looks like this:
First things first: The layout 17
Figure 6. Unstyled <header> and <footer>
Not quite what we had in mind! The <header> and <footer> are both 100% wide which is great,
but we need to give them positions and heights (with tab content taking up the remaining space
inbetween). Also let’s get rid of the PhoneGap background gradient and put our own background
colours in. Also let’s take out the forced uppercase. Change the body clause in index.css to look
like this:
body {
-webkit-touch-callout: none; /* prevent callout to copy image, etc w
hen tap to hold */
-webkit-text-size-adjust: none; /* prevent webkit from resizing text to
fit */
-webkit-user-select: none; /* prevent copy paste, to allow, change
'none' to 'text' */
font-family:'HelveticaNeue-Light', 'HelveticaNeue', Helvetica, Arial, sans-serif;
font-size:12px;
height:100%;
margin:0px;
padding:0px;
width:100%;
}
Then add a clause for header like this:
First things first: The layout 18
header {
background-color:#555; /*medium grey*/
color:#ccc; /*slightly greyish white*/
height:40px;
line-height:40px; /*height of a *text* line*/
}
Then add a clause for footer like this:
footer {
background-color:#555; /*medium grey*/
color:#ccc; /*slightly greyish white*/
height:20px;
line-height:20px;
}
Running this looks like:
Figure 7. <footer> is too high
Hmm, the footer isn’t at the bottom! Let’s position it absolutely and make it flush with the bottom
of its parent (the document body). Add to the footer rule so that it looks like:
First things first: The layout 19
footer {
background-color:#555; /*medium grey*/
color:#ccc; /*slightly greyish white*/
height:20px;
line-height:20px;
position:absolute;
bottom:0;
width:100%; /*no default width for position:absolute*/
}
Running this looks like:
Figure 8. <footer> flush with bottom of document body
Great! Now let’s put our three tabs into the header. We’ll do it as an unordered list of links. Make
<header> of index.html look like this:
<header>
<ul id="tab-bar">
<li >
<a href="#search">Search</a>
</li>
<li >
<a href="#discover">Discover</a>
</li>
<li>
<a href="#write">Write</a>
</li>
</ul>
</header>
Running this looks like:
First things first: The layout 20
Figure 9. First attempt at tabs
Clearly a disaster! We need some styling to line up the list items horizontally in the header. Add
the following three clauses to the CSS file:
/*entire tab row*/
#tab-bar {
/*clear any inside and outside gap space*/
margin:0;
padding:0;
}
/*each tab*/
#tab-bar li {
display: inline; /*prevent each item from newlining*/
float:left; /*stack left*/
width: 33.3333%; /*have a third of total tab-bar space*/
}
/*tappable link in each tab*/
#tab-bar li a {
color: #ccc;
display: block; /*make "width-having"*/
font-weight: bold;
overflow: hidden; /*so long link text words get cropped*/
text-align: center;
text-decoration: none; /*remove default link underline*/
}
Running this looks like:
First things first: The layout 21
Figure 10. Tabs line up horizontally
Looking good! But the tabs need a few more things to look more useful. Namely, horizontal
dividers, icons and some kind of current tab highlight. For the horizontal dividers, let’s try giving
the second and third tabs a left border. CSS version 2 (the latest version being 3) has a nifty
selector where we can say “element type Y only where it follows an element type X”. With this
we can target any tab after the first one and apply a left border. Add the following clause to the
CSS:
/*a border-left for the middle and rightmost tab*/
#tab-bar li + li
{
border-left:1px solid #aaa; /*light grey*/
}
Running this looks like:
Figure 11. <header> too wide for document body
First things first: The layout 22
Ouch, that’s not a good look. What’s happened here is that the border has added 1px to the total
width of the second and third tabs. These tabs are now wider than a 3rd of the <header> row
and so the last tab gets bumped onto the next line. This is A Very Annoying Thing. One cheesy
little workaround for this is to use a simple background image to simulate the border. Make a 1
pixel wide by 16 pixel tall image in GIMP (or what-have-you) and floodfill with #aaaaaa which
is a very light grey. Export to a PNG image in assests/www/img called aaaaaa_16_v.png. Then
change the previously added CSS clause to look like this:
/*simulate a border-left for the middle and rightmost tab*/
#tab-bar li + li
{
background-image:url(../img/aaaaaa_16_v.png);
background-repeat:repeat-y;
background-position:left;
}
Running this looks like:
Figure 12. <header> fits nicely
Pretty good! OK, we’ll do the icons next. We want each tab to have a little icon on it. There are
millions of icon sets floating around these days. They tend to be one of three types:
• Always free
• Free only for personal use (else you should pay)
• Always paid-for
There’s also new school flat icons versus traditional deep icons. Design memes come and go but
we’ll go with something a little flat. We’ll use these rather nice ones which are royalty-free, free
for personal and commercial use:
http://www.graphicsfuel.com/2013/04/20-flat-icons-psd
Note that these icons are in PNG format which is a raster format. Raster icons are easy to use,
but can only be shrunk or enlarged by extracting or guessing information (respectively). This
means they only really look good at their native size which means that, depending on the pixel
First things first: The layout 23
density of the device display, they might be too tiny and hard to make out or really massive and
Legoish. But we’ll use them for simplicity.
One alternative would be to use a vector format - such as SVG - for the icons which stores the
image such that it can be scaled up or down without losing information. Another new trend is to
have the browser load something called an icon font. This is like a normal font but where each
character is an icon (remember Wingdings?!). This has the advantage that the icons are sizeable
just like any other text. Also they can be bolded or italicised. But they can only be of one colour.
Go ahead and put all of the PNG icons in assets/www/img (though we won’t use all of them).
Let’s reference some of these icons in our tab markup, change <header> in index.html to look
like this:
<header>
<ul id="tab-bar">
<li>
<a href="#search"><img src="img/search.png"> Search</a>
</li>
<li>
<a href="#discover"><img src="img/chat-bubble.png"> Discover</a>
</li>
<li>
<a href="#write"><img src="img/file.png"> Write</a>
</li>
</ul>
</header>
Note the space after the image and before the link text. Running this gives:
Figure 13. Icons we sourced are way too big
Woah, those icons are pretty big eh? The icons are a mix of square, tall or wide, but they all have
a biggest side of about 128 pixels. That’s clearly way too big for us here. Let’s use GIMP to resize
search.png, chat-bubble.png and file.png to have a biggest side of 16px - the same as our app
font size (in index.css) [NOTETOSELF double check this]. So go ahead and make those changes
and overwrite the original icon files. While you’re at it, do the same for paste.png because we’ll
be using that later on. (Feel free to trash the other icon files from assets/www/img as we won’t
First things first: The layout 24
be needing them in this little app.) Those scalable icon formats are looking real attractive now
huh?
After changing the icon sizes, it looks like this:
Figure 14. Icons at correct size
Not bad at all. But hmmmm, don’t you think the icons look a little out of whack? Like they’re
slightly higher than the line of text? We can remedy this by adding to the CSS:
a img {
vertical-align:middle; /*make more sensible relative to text baseline*/
}
(Yes, we could do these icons as CSS background images but what the heck.) That’s better. We
restrict this only to images in <a>’s so we don’t screw up any other images we might have in the
markup.
All we need now is a highlight for the currently selected tab, and while we’re at it we should
choose our default tab that we want to be displayed first on app load. Let’s plump for the Search
tab. Add a class name of “current” to the Search tab thus:
<ul id="tab-bar">
<li class="current">
<a href="#search"><img src="img/search.png"> Search</a>
</li>
.
.
</ul>
Then, in the CSS, modify #tab-bar li{} and add #tab-bar li.current{} thus:
First things first: The layout 25
/*each tab*/
#tab-bar li {
display: inline;
float:left;
width: 33.3333%;
border-bottom:3px solid #555; /*same bg as header*/
}
/*current tab*/
#tab-bar li.current {
border-bottom:3px solid #990000; /*signature red*/
}
We simply add a bottom border, in our signature red, to any tab bar list item that has a class of
“current”. We also add a border of the same size but using the header’s background colour to non
current tabs. This keeps everything looking flush horizontally. Later on (soon actually!) we will
use JavaScript to detect tap events on the tabs and change the current tab. Running what you
have so far looks like:
Figure 15. Current tab highlight
Pretty good! Only two little things are bugging us now. The content area text starts a little too
close to the tab bar, and, thinking about it this app doesn’t really need a footer at all! Change the
HTML footer to simply look like this:
<footer></footer>
Then add .japxlate_app{} to the CSS and also change the height of footer{} thus:
.japxlate_app {
padding-top:1em; /*move content away from tab bar*/
}
footer {
background-color:#555;
color:#ccc;
height:2px; /*down to 2px from 20px*/
line-height:20px; /*no longer meaningful...*/
First things first: The layout 26
position:absolute;
bottom:0;
width:100%;
}
Running this looks like:
Figure 16. Final app layout
Which we’ll stick with for the rest of the tutorial - and app! We have a 2px footer which is a bit
gimmicky, but will help us a bit with scroll debugging a bit later on. The tab content text is now
one newline(ish) down from the tab bar.
..
To fullscreen or not?
You might have noticed by now that the default PhoneGap app, and our own app’s layout that
we’ve just finished, fill the entire screen of the device. Even the Android status bar (which
shows the time, battery charge and signal strength etc) is obliterated.
Game apps tend to fill the entire screen, but almost every utility app out there leaves the
status bar. The good news is that we can get the status bar back quite easily by opening
PROJECTROOT/platforms/android/res/xml/config.xml and changing:
<preference name="fullscreen" value="true" />
to
<preference name="fullscreen" value="false" />
and then re-running the app.
You can choose which style you like and the rest of this tutorial is valid either way. Note that
figures showing device screenshots won’t have the status bar.
7. First things first: The tabbing
mechanism
The layout is in the bag now, but we need a mechanism to markup the content for our three
different tabs and a way for taps on the tabs to trigger the display of the relevant content.
We can markup the content for all three tabs in the HTML file and simply have Discover and
Write hidden (Search is our default remember) with CSS when the app first starts. Let’s do this
first before we look at any JavaScript. Edit <div class="japxlate_app"> in index.html so that
it’s contents are like this:
<div class="japxlate_app">
<div id="tab-content">
<div id="search" class="current">
search tab content. search tab content. search tab content.
search tab content. search tab content. search tab content.
search tab content. search tab content. search tab content.
search tab content. search tab content. search tab content.
</div>
<div id="discover">
discover tab content. discover tab content. discover tab content.
discover tab content. discover tab content. discover tab content.
discover tab content. discover tab content. discover tab content.
discover tab content. discover tab content. discover tab content.
</div>
<div id="write">
write tab content.write tab content. write tab content.
write tab content.write tab content. write tab content.
write tab content.write tab content. write tab content.
write tab content.write tab content. write tab content.
</div>
</div>
</div>
Then let’s default to hidden, but with class="current" being visible, for these <div>s in
#tab-content. Add the following two clauses to index.css:
First things first: The tabbing mechanism 28
#tab-content > div.current {
display:block;
}
#tab-content > div {
display:none;
}
Hmm, well running this looks like:
Figure 17. Tab content spills over the footer
Search tab is indeed the only visible tab, but if there is a lot of content then it overflows and goes
past the footer! This will cause our PhoneGap app to be swipe scrollable which is a bad thing!
To fix this, let’s see what the .japxlate_app master container <div> is doing in relation to the
footer when it has both little and lots of content. For that let’s add this cheeky little debug to the
.japxlate_app{} CSS:
.japxlate_app {
padding-top:1em;
border:1px solid green; /*debug*/
}
This puts a thin green border around the entire div. This is a useful debugging tool but note that
it will add two pixels to the width and two pixels to the height of the div it is applied to. This
may make scrollbars appear where usually you wouldn’t have scrollbars.
Running with both large and small amounts of content looks like this:
First things first: The tabbing mechanism 29
Figure 18. Size of .japxlate_app div with large (left) and small (right) content amounts
So it looks like our master container div doesn’t have a fixed height and is as tall as it needs to be
for its content. We want it to be exactly tall enough to fit perfectly under the header and above
the footer. Then, if content is lots and it overspills, it will clip above the footer and won’t screw
up our app’s look and feel. We may then choose to handle content scrolling manually.
Our .japxlate_app master container div has the same parent as the header and footer (ie.
<body>) so we should be able to position it absolutely, tinker with CSS top and bottom properties
and “slot” it in between the header and footer. Let’s change the CSS for .japxlate_app to look
like this:
.japxlate_app {
padding-top:1em;
border:1px solid green;
overflow:auto; /*scrolling functionality *IF* we need it*/
position:absolute;
top:43px; /*flush with bottom of header*/
bottom:2px; /*flush with top of footer*/
width:100%;
}
Note that we’re keeping the debug green border for the moment. Running the app now looks
like this:
First things first: The tabbing mechanism 30
Figure 19. Improved .japxlate_app div with large (left) and small (right) content amounts
For the win! Notice how (on desktop Chrome only) we only get the scrollbar when we need it.
Notice also how it’s a scrollbar just for the content div and not a full scrollbar for the entire
document. This is great for our app because users won’t be able to whiz it around the screen like
a normal browser page. As we’ll see later though, we will annoyingly have to implement our
own scrolling for this content pane on the device. Go ahead and strip out that border:1px solid
green; statement for the .japxlate_app{} rule.
You must be exhausted with CSS things now (I know I am!), so let’s move on to the very last first
thing (say what?!) - which is the behaviour for the tab tapping which we’ll implement in good
ol’ JavaScript. We need to do two things here:
1. Detect a tap on a tab
2. Load / display content for that tab (hiding the previous tab’s content at the same time)
If you’ve been debugging the app in Chrome so far (I have!), here’s where we hit a tiny
stumbling block. If you remember our default index.js, all of the magic happens after we
catch the deviceready event. This is a PhoneGap event that desktop browsers won’t fire. An
advanced way to get around this would be to look at something like Stopgap (though, at the
time of writing, this is looking a bit tumbleweedy) or, more straightforwardly, some hacks
like at http://stackoverflow.com/questions/6687099/how-to-fire-deviceready-event-in-chrome-
browser-trying-to-debug-phonegap-projec.
What we want to do, for desktop browsers, is to not load phonegap.js. Then, instead of waiting
for the deviceready event to execute our x_y_z(), we simply call x_y_z() as soon as the browser
DOM is ready. Let’s use the solution by Chemik at the aforementioned StackOverflow page to
only load phonegap.js on condition of being on a mobile device. We can do this in index.html
thus:
First things first: The tabbing mechanism 31
.
.
<footer></footer>
<!--load phonegap.js only if on mobile device-->
<script type="text/javascript">
if (navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry|IEMobile)/)) {
var line = '<script type="text/javascript" src="phonegap.js"' + '></'+'script>';
document.writeln(line);
}
</script>
.
.
Note that we break up the ending </script> in our string so that it isn’t picked up by the
(WebView) browser - or our IDE - as an actual ending script tag! This code will now only load
phonegap.js for mobile devices. You can test this by - carefully! - inserting a cheeky alert('I am
phonegap.js'); right at the top of phonegap.js. Don’t forget to remove this alert when you’ve
finished testing!
So now we only have phonegap.js loaded on an actual mobile device. This gives us a little tool to
help with the deviceready event problem. Edit bindEvents() and receivedEvent() in index.js
to look like this:
.
.
// Bind Event Listeners
//
// Bind any events that are required on startup. Common events are:
// 'load', 'deviceready', 'offline', and 'online'.
bindEvents: function() {
if (window.cordova) { //actual app
document.addEventListener('deviceready', this.onDeviceReady, false);
} else { //debugging in desktop browser
this.onDeviceReady();
}
},
// Update DOM on a Received Event
receivedEvent: function(id) {
console.log('Received Event: ' + id);
},
.
.
If phonegap.js is loaded, it will define the window.cordova object which we can test for before
setting up our event listener. If phonegap.js is not loaded, we simply call what the listener calls
anyway. Running this in both desktop Chrome and your device should produce the eventual
console.log() message (you’ll see this via Eclipse’s LogCat if running on your device).
First things first: The tabbing mechanism 32
..
All about alerts (and PhoneGap API plugins)
Since we’re talking about debugging and JavaScript alert()s and things, let’s talk about how
we can use PhoneGap to produce more native-like alerts. JavaScript alerts will definitely give
your app that non-nativey, browser app feel. In fact, using alert() even on desktop sites is
considered a bit naff these days!
Conveniently, PhoneGap exposes a Notification API for “Visual, audible, and tactile device no-
tifications.” The documentation at http://docs.phonegap.com/en/3.1.0/cordova_notification_-
notification.md.html says we can use it like this:
First things first: The tabbing mechanism 33
..
navigator.notification.alert(message, alertCallback, [title], [buttonName]);
So let’s try that. Stick navigator.notification.alert('Some alert message', null); in the
receivedEvent() function that we were just tinkering with. Running this (which obviously
won’t work in desktop Chrome) gives a spurious error in LogCat:
Figure 20. Error when attempting navigator.notification.alert()
What’s going on? Well, it turns out that “As of version 3.0, Cordova implements device-level
APIs as plugins”. We have to install whichever APIs we want in our project. This removes
bloat as, previously, all APIs came pre-installed in every PhoneGap project. I actually found
this to be a bit mysterious and poorly documented (I found myself mashing up a mix of info
from Cordova docs and PhoneGap docs). But here’s how to add a particular plugin to your
PhoneGap project. Go to anywhere in your project folder structure on the command line and:
First things first: The tabbing mechanism 34
..
you@yours$ japxlate]$ phonegap local plugin add https://git-wip-us.apache.org/repos/asf
/cordova-plugin-dialogs.git
From PhoneGap v3.3.0 you can simply type phonegap local plugin add
org.apache.cordova.dialogs
Which should echo:
First things first: The tabbing mechanism 35
..
[phonegap] adding the plugin: https://git-wip-us.apache.org/repos/asf/cordova-plugin-di
alogs.git
[phonegap] successfully added the plugin
(Note that you won’t need to run this command, and you won’t get the above error, if you’ve
gone down The fiddly older way as that bundles all plugins into your project).
You’ll get the relevant URL from the docs for whichever plugin at the “API Reference” section at
http://docs.phonegap.com/en/3.1.0/ (PhoneGap has a good list of core and 3rd party plugins at
https://build.phonegap.com/plugins but the installation instructions for each one are seemingly
out-of-date and mention tinkering with XML config files which we don’t need to do after
running the above command.) The above command has downloaded the source for the plugin
and put it in /assets/www/plugins (in this case in org.apache.cordova.dialogs)
but diff on v3.3.0 etc.
It has also added references to the plugin in /assets/www/cordova_plugins.js - a file which
has been there from the start but just as a placeholder stub. The phonegap.js that we include
in our index.html actually also includes cordova_plugins.js so after running the above
command, we have all we need to start using navigator.notification.alert()! Try it again!
It works!:
First things first: The tabbing mechanism 36
..
Figure 21. Default navigator.notification.alert()
Great. But hmmm, it looks just the same as a normal JavaScript alert()! Currently it does
yes, but the advantage is that we can customise the title and button text. We can also specify a
callback function to trigger when the button is tapped. Try:
First things first: The tabbing mechanism 37
..
navigator.notification.alert('Some alert message', null, 'The title', 'Oki doki');
Running this looks like:
Figure 22. Customised navigator.notification.alert()
For the win! If you want to go forward with these customised alerts, keep in mind that they
won’t work on desktop Chrome so you may need to write a little wrapper function to still be
able to debug on desktop Chrome. The Japxlate app won’t be alerting anything to the user
on purpose - perhaps just some important error messages. Therefore we’ll go forward in this
tutorial with plain vanilla JavaScript alert()s. But I wanted to show you the general plugin
mechanism on what is no doubt one of the easier to use plugins. In fact, I’m not done yet!:
First things first: The tabbing mechanism 38
..
you@yours$ japxlate]$ phonegap local plugin list
[phonegap] org.apache.cordova.dialogs
This command lists all plugins installed in the current project.
you@yours$ japxlate]$ phonegap local plugin remove org.apache.cordova.dialogs
[phonegap] removing the plugin: org.apache.cordova.dialogs
[phonegap] successfully removed the plugin
This command removes the specified plugin from the current project. You specify the plugin
by its reverse-DNS identifier. You can find these out by issuing the above “list” command.
There are plugins to access the mobile device’s camera, accelerometer, phone contacts and
many more. Using these plugins is how we make a full fat mobile app and not just a simple
website-in-a-box.
PhoneGap v3.3.0 also has “Plugman” which is another way of working with plugins.
Plugman lets you add or remove plugins for one specific platform, whereas the above
method will add or remove plugins globally to any and all platforms used in the project.
Please see http://docs.phonegap.com/en/3.3.0/plugin_ref_plugman.md.html.
We’ve just been able to simulate our deviceready event on desktop Chrome for debugging and
we are ready to get our tab taps working. receivedEvent() in index.js is where the magic
happens because by the time we reach there, the device is ready (and the browser DOM is ready
as we’ve put JavaScript includes at the bottom of our HTML). But let’s not go down the route of
stuffing all of our JavaScript in index.js. Let’s go modular - right from the start. Create a new
JavaScript file called:
japxlate.js
in /assets/www/js
and include it from index.html thus:
<script type="text/javascript" src="js/japxlate.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript">
app.initialize();
</script>
Put a function called configureTabs() in the newly created japxlate.js thus:
First things first: The tabbing mechanism 39
//tab clickability
function configureTabs()
{
var tabs = document.querySelectorAll("#tab-bar li a");
for(var loop = 0; loop < tabs.length; loop++)
{
var tab = tabs.item(loop);
tab.addEventListener('click', function(event){alert(event + ' on ' + this);}, f
alse);
}
}
Then modify index.js to call this new function in receivedEvent() thus:
// Update DOM on a Received Event
receivedEvent: function(id) {
console.log('Received Event: ' + id);
configureTabs();
},
Running this, and clicking on one of the tabs results in:
First things first: The tabbing mechanism 40
Figure 23. Debug alert() after clicking Discover tab
We are nearly there! We are detecting tab taps nicely! First let me explain some key points of the
configureTabs() function so far.
document.querySelectorAll("#tab-bar li a");
This is a great new piece of modern JavaScript that returns to us an array of DOM elements (a
“NodeList”) that match our CSS style selector. (The related querySelector() returns the first
matching element.) This is something that has found its way into W3C standard DOMJavaScript
based on something that jQuery has popularised (but not invented - Behaviour.js was one of the
first to do this).
Here we run querySelectorAll() on the document object so we are going to get all matches
contained in <body>. Usefully, it can also be run on an Element object - for example a certain table
or form - or a DocumentFragment element to only return matching elements in that particular
container element. #tab-bar li a is a CSS style query for “an <a> in a <li> in any element with
id ‘tab-bar’”.
We loop over all matching <a> elements and set a click handler using the DOM standard
addEventListener() (as formally described at
http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration). Our event han-
dler in this case is a simple anonymous function giving a debug alert. In event handler functions,
First things first: The tabbing mechanism 41
an Event object is passed as a parameter and contains information about the particular event that
triggered the handler - screen x and y coordinates for mouse events and which key was pressed
for keyboard events and so on. In event handler functions, this refers to the element on which
the event happened.
Let’s replace the dummy click handler with something that we’ll actually want to use. But first,
remember that in the click handler function we only have the event object and the <a> object (as
this)? We’ll also need to know which content <div> relates to which <a>, then we can switch
the content accordingly. Modify the header of index.html to look like this:
<header>
<ul id="tab-bar">
<li class="current">
<a href="#search" data-div-id="search"><img src="img/search.png"> Search</a>
</li>
<li>
<a href="#discover" data-div-id="discover"><img src="img/chat-bubble.png"> 
Discover</a>
</li>
<li>
<a href="#write" data-div-id="write"><img src="img/file.png"> Write</a>
</li>
</ul>
</header>
HTML5 allows us to use custom or “data” attributes where we can add any attribute and value
we like to any particular element. The attribute names start with “data-“. Here we simply link
each <a> to its matching content <div> id. We’ll use this attribute (soon) in the click handler for
tabs.
OK, next strip out the dummy handler from addEventListener() and make it look like this:
tab.addEventListener('click', onclickForTab, false);
This will call the onclickForTab() function as a click handler. We define the onclickForTab()
function, in japxlate.js thus:
//set up and display a newly tapped tab
function onclickForTab(event)
{
//to prevent URL from changing and browse history building up
event.preventDefault();
//-------tab display logic---
var lastTab = document.querySelector('li.current a');
//NOP if clicking current tab again
if(lastTab == this)
First things first: The tabbing mechanism 42
{
return false;
}
lastTab.parentNode.className = ''; //undisplay
this.parentNode.className = 'current';
//---------------------------
//-----content div display logic---
var lastDiv = document.querySelector('div.current');
lastDiv.className = ''; //undisplay
var matchingDiv = this.getAttribute('data-div-id');
var thisDiv = document.getElementById(matchingDiv);
thisDiv.className = 'current';
//-----------
//get tab div id from tab link
var divId = this.getAttribute('data-div-id');
}
Let’s go through this code, which looks fiddly at first, but basically tinkers with CSS class names
such that things turn on and off as we want.
The first thing we do is the DOM standard preventDefault() which prevents the browser’s
default action for the event from triggering. The default browser action for clicking on a link is
to:
1. Change URL in address bar to that of link target
2. Add new URL to browsing history
3. Load new URL
As our links are simply triggers to load tabs and not proper links, we don’t want any of these
steps to happen. Step [2] is especially annoying. If we don’t call preventDefault() for our tab
taps, if we open our app and click on the tabs ten times, we will have to use the device’s BACK
button ten times to exit the app!
Next we use querySelector() to get the single current tab link. Because ‘this’ in our click handler
will be the clicked element, we can do a check to see if this is the same as the previous current
tab. And if so, do a “no operation” (NOP). We then manipulate classnames to activate only the
clicked tab.
Similarly, we use querySelector() to get the currently active content <div>. We activate the
content <div> for the clicked tab by retreiving data-div-id from the clicked <a> and using that
to get the correct div.
First things first: The tabbing mechanism 43
Anyway, this all works!
Figure 24. Initial configureTabs() is working well
Thinking deeper and keeping an open mind, there’s more to our tabs than just displaying the
relevant content. A given tab might have to do some one-off initialising of a resource - perhaps
a database. Or some per-load checking of, eg, network availability on the device. We also might
like to add new tabs in future as users request more features. We might simply just want to
change the default tab based on user complaints!
We can cover all of these bases with a few simple steps. First, alter the bottom of onclickForTab()
to look like:
.
.
//get tab div id from tab link
var divId = this.getAttribute('data-div-id');
onclickForNamedTab(divId);
}
onclickForTab() is a generic handler for any tab tap, but we are adding onclickForNamedTab()
to handle tab specific initialisation. Put onclickForNamedTab() in japxlate.js and it looks like
this:
First things first: The tabbing mechanism 44
//Do the one-off loading and everytime setup for whichever tab
function onclickForNamedTab(divId)
{
if(divId == 'discover')
{
onclickForTab_Discover();
}
else if(divId == 'search')
{
onclickForTab_Search();
}
else if(divId == 'write')
{
onclickForTab_Write();
}
}
We simply switch on the tab content <div> id, calling the appropriate onclickForTab_theTab().
Yes, you’ve guessed it, if you want to add more tabs to the app, you will have to update this
switch case (and add the corresponding onclickForTab_theNewTab()). This function is a simple
dispatcher to other functions that are going to do the actual one-off and per-load initialisations
for tabs.
For a “one-off” initialisation, we are going to have to somehow record which tabs have been
opened so far. We’ll do this using a global variable. Eek! Global variables are not current best
practice for JavaScript, but we’ll do it to keep this small and simple app, er, small and simple. Put
this at the top of japxlate.js:
//Has the first load of each tab happened yet?
var global_pagesLoaded = {discover:false, search:false, write:false};
We can then check - and set - these values in our onclickForTab_theTabName() functions that
our onclickForNamedTab() dispatcher calls. Let’s get started with the first of these functions for
our Discover tab. Put this in japxlate.js:
//One-off loading and each time setup for discover tab
function onclickForTab_Discover()
{
//console.log('click on discover tab');
if(!global_pagesLoaded.discover)
{
firstLoadForTab_Discover();
}
//each time setup to go here
}
First things first: The tabbing mechanism 45
We simply check if global_pagesLoaded.discover is false and if so call firstLoadForTab_-
Discover(). We also have a space here for any “each time” setup of the Discover tab. Go ahead
and create functions, using this one as a template, for the Search and Write tabs (do a copy paste
and then change ‘Discover’ to ‘Search’ and ‘discover’ to ‘search’ and etc). We’ll modify these
functions later if we need to.
OK, we still need firstLoadForTab_Discover() which will perform one-off initialisation for the
Discover tab. Do it like this, again in japxlate.js:
//One-off loading for discover tab
function firstLoadForTab_Discover()
{
//console.log('first load for discover tab');
global_pagesLoaded.discover = true;
//one-off setup to go here
}
All we do is set global_pagesLoaded.discover to true so that this function does not get
called again from onclickForTab_Discover() when the tab is tapped a subsequent time. At
the moment this is just a placeholder for whatever we might need down the line. Like we just
did for onclickForTab_*(), replicate this function for the Search and Write tabs.
If we temporarily uncomment the console.log() calls, running this - and clicking tabs randomly
- shows that we do indeed have a first load that fires only once and a click that fires each time.
Figure 25. One-off tab loading is confirmed
Done and dusted. Money in the bank. Move along, nothing to see here… right? Well there’s
just one thing missing. If you’ve really really been paying attention and thinking one or two
steps ahead perhaps, you may have noted that our setups (one-off and each time) for the default
Search tab are only fired if we click off that tab and then back on it. Clearly this is not useful
and whichever tab is set to be the default needs to have its setups run right off the bat. Let’s
solve this problem by, on deviceready, calling a little function to retreive the current tab
and calling our already existing onclickForNamedTab() dispatcher for that tab. Add a call to
initialiseDefaultTab() at the bottom of receivedEvent() in index.js so that it now looks
like this:
First things first: The tabbing mechanism 46
// Update DOM on a Received Event
receivedEvent: function(id) {
console.log('Received Event: ' + id);
configureTabs();
//load and show whatever we've set the initial tab to be
initialiseDefaultTab();
}
Then define initialiseDefaultTab() in japxlate.js thus:
//Load and show our default initial tab
function initialiseDefaultTab()
{
var defaultTab = document.querySelector('div.current');
var divId = defaultTab.id;
onclickForNamedTab(divId);
}
We use querySelector() to get whichever content <div> has been set as current in the HTML
markup. We could in theory select the tab that has been marked as current but, as that will be
in sync with the content div anyway, it is academic.
Congratulations, you have just built a working infrastructure for the Japxlate app! This is a good
starting point for any simple PhoneGap app.
8. The Search tab
8.1 Layout and interface
The Search tab - the first tab that the user will see when launching our app - is going to be
a search form for the user to search our Japanese dictionary. It will also display any and all
matching results in a scrollable area.
We’ll have a rule that the user’s search query can be in Japanese as well as English. Not only will
this increase the usefulness of our app, it will also enable a future “reversing” of the app to be
localised for Japanese speakers wanting to learn English vocabulary. Let’s have another rule that
they can type the Japanese or English query into the form in the same input box and without
having to fiddle with radio buttons or other such inputs (which are a bit old hat for search forms
anyway but especially cumbersome on mobile devices). With these rules and functionalities in
mind, a wireframe of the Search tab might look like:
Figure 26. Quick wireframe of the Search tab layout
OK, let’s markup - and then style - the search form and the results space for dictionary queries.
Mosey on down to http://www.ajaxload.info and make a “loading” spinner image (gif) for the
Search tab. I made mine use the Japxlate signature red (#990000) and a transparent background.
Download it and put it in /assets/www/img as spinner.gif.
Let’s markup the form and results space - in index.html - like this:
The Search tab 48
<div id="search" class="current">
<button type="button" id="search-button" style="float:right; width:45%; margin-righ
t:1%;">
<img src="img/search.png">
Search
<img id="button-spinner" src="img/spinner.gif" style="visibility:hidden;">
</button>
<input type="text" id="search-query" placeholder="Japanese or English" size="40"
style="width:45%; margin-left:1%;">
<br>
<span id="loading-text">
[Loading core dictionary. This takes a while the first time.
<img src="img/spinner.gif">]
</span>
<div id="results-wrapper">
<div id="search-results">
You can search by kanji, hiragana, katakana, English or romaji!
</div>
</div>
</div>
We float our search button right (which means that in the markup it has to come before things on
the same line that would be visually to the left of it) but make it 1% (of total width) away from the
edge for nice appearance. We reuse search.png as a button icon. We also include the spinner.gif
that we just created but default it to visibility:hidden. Why not just display:none? Because
with visibility:hidden, it is hidden but still takes up space in the layout flow. This means the
layout won’t “jump” when we make it appear. We’ll switch this image’s visibility on and off
programmatically.
Then we’ve got our text input which uses the new HTML5 placeholder attribute to present a
hint or instruction to the user about what kind of entry it expects. The text input is also 45% wide
with an edge spacing of 1%.
Why not just make both 50%? Because then they will touch in the middle which will end in tears
with big fingers on a small display!
We then have a “this will take a while” message and spinner that we will remove after one-off
setup is complete.
Finally we have a container for our search results - <div id=”search-results”> - which displays a
default search hint. We also have a wrapper for the search results container - <div id=”results-
wrapper”> - which is going to be the scroll viewport for search results. These two divs need the
following styles in index.css:
The Search tab 49
#results-wrapper {
position:static;
width:100%;
margin-top:1em; /*space one <br>(ish) from bottom of search form*/
overflow:hidden;
}
#search-results {
position:relative; /*we position this relative to its *normal* position*/
top:0; /*but set the normal top position anyway. We will*/
width:100%; /*change this top value to affect a scroll*/
}
The keypoint here is position:relative; on the search-results div which means that we will be
able to position it (ie. scroll it) relative to an unmoving parent - the results-wrapper div. Running
this looks like:
Figure 27. Initial appearance of the Search tab
Not bad. Just two grumbles here.
1. The height of the text area is lacking and also it’s shorter than the button. Let’s even these
two out. (Actually on my device the text input and the button don’t seem to be on the
same baseline!)
The Search tab 50
2. Icon for search is screwy again - let’s fix that like we fixed the tab icons.
In index.css, change the existing:
a img
{
vertical-align:middle; /*make more sensible relative to text baseline*/
}
to:
a img, button img
{
vertical-align:middle; /*make more sensible relative to text baseline*/
}
Which covers (2). To fix (1), add this to index.css:
input[type="text"], button {
height:30px;
margin:0;
}
Running looks like this:
The Search tab 51
Figure 28. Improved appearance of the Search tab
Better!
8.2 Creating the database
Now, let’s also have a rule to the effect of dictionary searches working even when the mobile
device is offline. That is to say the app must use some kind of local storage on the device itself or
the WebView browser. Well, it turns out that the Android WebView supports something called
Web SQL which is a small, local implementation of an SQL database (specifically SQLite) in the
browser. We can load our Japanese dictionary into a client-side database and, based on the user’s
search term, query it in whichever way we need to pull out matches.
..
Important note about Web SQL
Web SQL is an abandoned specification (see http://www.w3.org/TR/webdatabase/) that W3C
no longer maintain, and I do not recommend that you use it going forward in your owns apps!
W3C’s beef was that it was only being implemented using SQLite - obviously they aren’t in
the business of standardising a piece of vendor lock in! For similar reasons Mozilla (ie. Firefox
browser) have chosen not to implement it right from the start. I do kind of agree that bringing
a heavy server-side thing to the client is a bit of an odd move. In fact, traditional SQL on the
The Search tab 52
..
back-end is somewhat in crisis itself these days in the world of NoSQL datastores. Though it is
very useful for mobile apps that might not be online and need to work with some data.
Why are we using it for this tutorial?
Somewhat for historical reasons but also because I know it will be perfect for fuzzy text
searching. I know from experience that it will “just work”. When using PhoneGap we are lucky
too because “Cordova provides access to both interfaces (Web SQL and something else called
Web Storage) for the minority of devices that don’t already support them. Otherwise the built-
in implementations apply.”
What would be some alternatives?
Ignoring PhoneGap and the world of mobile apps, Indexed DB (a W3C standard at
http://www.w3.org/TR/IndexedDB/) looks to be picking up steam. Though caniuse.com tells
me that support is currently less than that of Web SQL. Also it hasn’t made its way into
PhoneGap at the time of writing. Indexed DB mirrors the more modern style of NoSQL
databases closely.
I hope that future versions of the app (and this tutorial) can use Indexed DB.
PhoneGap v3.3.0 now supports Indexed DB, but only if the underlying WebView
supports it. At the time of writing this means only Windows Phone 8 and BlackBerry
10.
PhoneGap’s (well actually Cordova’s) Web SQL docs are at
http://docs.phonegap.com/en/3.1.0/cordova_storage_storage.md.html As you can see, it’s a fairly
small implementation of an SQL database. But writing for it in JavaScript with callbacks was a
novelty for this grizzled MySQL hacker!
OK, let’s crack on now with Web SQL initialisation for the first load of the Search tab. Stick this
cheeky call - to a function we’re about to create - at the bottom of firstLoadForTab_Search()
in japxlate.js:
tryPopulateDB();
Let’s create this function, and other functions to do with general Web SQL setup, in a new file
in /assets/www/js called websql_core.js. Create this file, and the first function we’ll put in it
is the tryPopulateDB() we’ve just referenced. It will look like this:
The Search tab 53
//Open / create the "Japxlate" Web SQL database and - if it's not already
//present - create and populate the "edict" table
function tryPopulateDB()
{
//version 1.0, 4 megabytes
var db = window.openDatabase("Japxlate", "1.0", "Japxlate DB", 4 * 1024 * 1024);
db.transaction(checkDB); //only populate edict table if it not already exist
}
PRO TIP: The Cordova docs on Web SQL are going to be very useful to reference
when following this chapter. They are at http://docs.phonegap.com/en/3.1.0/cordova_-
storage_storage.md.html.
The same page for PhoneGap v3.3.0 removes the Web SQL reference, which to be
honest had at least one mistake in it, and instead points you to have a look at
http://www.html5rocks.com/en/features/storage.
We open a Web SQL database called Japxlate, at version 1.0, with a display name of “Japxlate DB”
and a size of 4 megabytes. I know from tinkering with the dictionary database for the @japxlate
Twitter channel that the core dictionary definitions will fit in 4 megabytes with a bit to spare.
Then we call transaction() on the returned database to run the query or queries in the
checkDB() function that we’re about to implement.
Now’s a good time to talk about the schema we’ll use for the dictionary table. We’ll call the table
“edict” as that’s the name of the Japanese dictionary that powers it
(at http://www.csse.monash.edu.au/∼jwb/wwwjdicinf.html#dicfil_tag) and the fields will be:
edict(id unique, kanji, kana, definition)
“id” will be an integer and a unique key to each record. “kanji” will hold the Chinese characters
that the word is written in. “kana” will hold the Japanese phonetic script that the word is written
in. Finally “definition” will hold one or more English language definitions for the word, separated
by ‘/’.
Our checkDB() function needs to know if the edict table exists and is full. If not, create it and fill
it.
The checkDB() function will receive a SQLTransaction object as a parameter from db.transaction().
Again in websql_core.js, make checkDB() look like this:
The Search tab 54
//Check if "edict" table exists and has records
function checkDB(tx)
{
//console.log('checkDB()');
tx.executeSql('SELECT COUNT(id) AS count FROM edict', [], successCheckDB, errorChec
kDB);
}
We call executeSql() on the received SQLTransaction object which needs at least an SQL query
as its first argument (and parameter values as the 2nd parameter if the query in the first argument
uses parameter binding), but can optionally take both a success and failure callback as 3rd and
4th parameter respectively. Here we run a very simple query to get the count of rows - by id
- in the edict table. This query will throw an error if the edict table does not exist (but not if
it exists and is empty which is a condition we will knowingly ignore for this simple app). We
don’t use parameter binding in this query so we provide an empty array as the 2nd parameter
simply because we need to “get” to the 3rd and 4th parameters. We specify an error and a success
callback. Should the query fail we can assume that the table does not exist and therefore needs
to be created and populated. Let’s look at the success callback first as it’s simpler and only has
to clear the “database loading” message:
//Callback for if checkDB() succeeds - ie. "edict" table present and full
//SO clear the "database loading" message
function successCheckDB(tx, results)
{
//console.log('edict already loaded');
document.getElementById('loading-text').innerHTML = '';
}
Pretty easy and not worth explaining other than to point out that the callback function receives
an SQLTransaction and an SQLResultSet object respectively.
Let’s get started on the error callback:
//Callback for if checkDB() fails - ie. no "edict" table
//SO create it and fill it
function errorCheckDB(transaction, error)
{
console.log('edict table not exist - will create and fill');
//here we need to do something to fill the table
}
This code so far will run without errors (but don’t forget include websql_core.js from
index.html (above the japxlate.js include)) but won’t do anything useful. It will get to the
“edict table not exist - will create and fill” log message and then stop. In the error callback, we
need to run another transaction on the Japxlate database which will load all the dictionary data
we need. Change errorCheckDB() to look like this:
The Search tab 55
//Callback for if checkDB() fails - ie. no "edict" table
//SO create it and fill it
function errorCheckDB(transaction, error)
{
console.log('edict table not exist - will create and fill');
//version 1.0, 4 megabytes
var db = window.openDatabase("Japxlate", "1.0", "Japxlate DB", 4 * 1024 * 1024);
db.transaction(populateDB, errorWebSQL, successPopulate);
}
We open the same Japxlate database and try to run the populateDB() queries on it. We have new
success and error callbacks. populateDB() looks like this:
//Create and fill the "edict" table
function populateDB(tx)
{
console.log('creating and filling edict table');
//DROP if present (ie. because it's present but empty)
tx.executeSql('DROP TABLE IF EXISTS edict');
//create
tx.executeSql('CREATE TABLE IF NOT EXISTS edict(id unique, kanji, kana, definition)
');
websqlEdictInserts(tx); //see websql_edict_inserts.js
}
We create the table according to our schema - DROPing it first just in case and so the
CREATE doesn’t fail. Finally we call websqlEdictInserts() which is a function we’ll put
in another JavaScript file. The websqlEdictInserts() function accepts an SQLTransaction
object and essentially runs a huge list of INSERT queries on it to populate our table. This
function isn’t very do-at-homeable because it’s basically a dump of the most common words
from the @japxlate Twitter feed’s database. If you are following this tutorial step by step,
please get the file /js/websql_edict_inserts.js from the app’s GitHub repository and stick
it in your /assets/www/js folder. To explain it a little bit more, here’s an excerpt from
/js/websql_edict_inserts.js:
The Search tab 56
function websqlEdictInserts(tx)
{
tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(5,",,
/curry/rice and curry/)');
tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(21,,,
/to blow (one's nose)/)');
tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(119,,
,/1000 yen/)');
tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(138,,
,/ten percent/)');
.
.
}
Note that the ID numbers aren’t in sequence because these words are the most common 20,000
or so words from @japxlate’s Edict dictionary which has nearly 200,000 entries!
OK, that’s populateDB() in the bag. But don’t forget errorCheckDB()’s custom error and success
callbacks. Let’s do the error callback first:
//Generic SQLError handler (for both db.transaction() and tx.executeSQL())
function errorWebSQL(transactionOrError, errorOrNull)
{
var error = null;
if(typeof transactionOrError == 'SQLTransaction') { //from tx.executeSQL()
error = errorOrNull;
} else { //from db.transaction()
error = transactionOrError;
}
console.log(error); //error is now an SQLError object
alert(Error processing SQL:  + error.code);
}
Ouch! This looks a bit over-complicated. What’s going on? Well, I didn’t realise at first,
and I only discovered it on a hunch, but we can reference error callbacks from both the
database.transaction() and transaction.executeSQL() methods (as we are already doing) but in
each case they will receive different parameters! The PhoneGap / Cordova docs for the Web
SQL API - at the time of writing - don’t seem to realise this and actually are therefore incorrect.
The PhoneGap v3.3.0 docs remove the entire Web SQL reference section.
This is something of a generic error callback and so we pull some strings to handle both cases.
Error callbacks as called from database.transaction() will receive (SQLError), and error callbacks
called from transaction.executeSQL() will receive (SQLTransaction, SQLError).
The Search tab 57
We simply alert out the code property of the received SQLError object. This is going to be our
recyclable Web SQL error handler going forward with the app.
The success callback for errorCheckDB() is going to do the same as the success callback for
checkDB() (which is successCheckDB()):
//Callback for if errorCheckDB() succeeds - ie. edict table populated OK
function successPopulate()
{
console.log('finished loading edict');
document.getElementById('loading-text').innerHTML = '';
}
Include websql_edict_inserts.js from index.html (above the include for websql_core.js)
and we are ready to go for a spin!
On first run, the “database loading” message and spinner take a few seconds to disappear, and
the log messages indicate database loading success. It looks like this:
Figure 29. First run of app with Web SQL database loading
Go ahead and run the app again after exiting it, the 2nd time around feels kind of faster right?
Let’s check the logs:
The Search tab 58
Figure 30. Second, faster run of app with Web SQL database loading
Woah! That’s right, Web SQL databases that you’ve created persist over multiple sessions of the
app (or browser). Pretty hot and tasty! This is a great reason why Web SQL, as abandoned and
awkward as it is, is really useful for mobile WebView apps as it can be used for saving things
offline.
8.3 Querying the database
Right, so that’s the database created, the table created, and the table filled. Phew!
We’re coming to the meat and bones of it now which is getting results from the database based
on the user’s search query. This will involve a bit of work on the frontend interface and a lot
of work on the backend. As we are kind of frazzled with Web SQL things right now, let’s get to
work on the frontend interface first.
Let’s make a new JavaScript file in /assets/www/js called search_interface.js to hold
anything to do with the frontend look and feel of searching. Right, one of the main things we’ll
want to do is to put search results from the database into the container div in our markup. Let’s
add a function to do this:
The Search tab 59
//Put the matching search results (which could be zero matches) on the page
function putResultsOnPage(results)
{
//get search results div
var theDiv = document.getElementById('search-results');
//clear current content
theDiv.innerHTML = '';
//might be no matches
if(results.rows.length === 0)
{
theDiv.innerHTML = 'No matches found in the common words dictionary.
Tweet @japxlate yourAdvancedWord for advanced word definitions.';
buttonSpinnerVisible(false); //stop the loading spinner
return;
}
//some results so loop through and print
for(var loop = 0; loop  results.rows.length; loop++)
{
var item = results.rows.item(loop);
var var theRomaji = item.kana; //TODO
var formattedDefinition = format_slashes(item.definition);
var defText = item.kanji + ' / ' + item.kana + ' (' + theRomaji + ') / ' + form
attedDefinition;
defText = defText.replace(new RegExp(global_searchTerm, 'ig'), 'span style=co
lor:#990000;$/span');
var defLine = 'img src=img/j.png style=vertical-align:middle; ' + defText
+ 'hr';
//var defLine = 'p class=def-line ' + defText + '/p'; //had CSS styling i
ssues (mostly text overflow)
theDiv.innerHTML += defLine;
}
buttonSpinnerVisible(false); //stop the loading spinner
}
We’ll expect to be passed an SQLResultSet object which will come from a successful query
on our Web SQL database. First we reset the current (ie. old) results by setting the container
div’s innerHTML property to empty. We then cover a scenario of no matches by printing a
“no matches” message (with a plug for the @japxlate Twitter bot!). Note that you can split up
very long quoted strings in JavaScript by ending lines with a ‘’. We then stop the “searching”
spinner by calling the buttonSpinnerVisible() function with a parameter of false. We’ll write
The Search tab 60
this function shortly and it’s basically a way to switch the “searching” spinner on and off. We
then return.
..
document.getElementById('some-id') versus
document.querySelector('#some-id')
You may be wondering why, for single elements, I am using document.getElementById('some-id')
and not the new fangled document.querySelector('#some-id'). Well it’s true that these will
both return the same element, and it’s true that getElementById() is a much older piece
of XML DOM, but the issue - at the time of writing - is one of performance (and perhaps
getElementById() is a teeny tiny bit more readable). After some benchmarking experiments
in desktop Chrome (using the mega useful console.time() and console.timeEnd() as at
https://developers.google.com/chrome-developer-tools/docs/console-api#consoletimelabel) I saw
that, for single elements, getElementById() was much faster than querySelector(). Out of
curiosity I also tested jQuery’s $('#some-id) (which returns a jQuery-specific list of nodes)
and found this to be much slower than the browser’s native querySelector(). Of note is that
the new jQuery v2.0 was much faster than v1.x for the same selector (though still slower than
querySelector()).
Now, if we’re still in the function we’ll have some results. We loop over and retrieve the results
using the SQLResultSet object’s rows.length property and rows.item(itemIndex) method.
What we do in the loop looks fiddly, but all we are doing is replicating the style of definition
lines that @japxlate uses. If you remember the snippet of websql_edict_inserts.js that we
looked at earlier, the format of the “definition” field in the database is “/definition one/definition
two/definition three/”. We want to space these multiple definitions out a bit more and remove
the lead and tail slashes; for that we’ll use a helper function called format_slashes() which also
goes in this file:
//Clean up the EDICT definition line that we get from our Web SQL DB
//For example, /one/two/three/ -- one; two; three
function format_slashes(slashesString)
{
//remove leading and trailing '/' characters
var string = slashesString.replace(/^//, ''); //leading
var string = string.replace(//$/, ''); //trailing
//change remaining '/' characters to a semicolon with space
return string.replace(///g, '; ');
}
We use JavaScript’s core replace() method to change the slashes based on regex matching. We
replace single lead and tail slashes with an empty string. We replace globally (the ‘g’ modifier
after the regex) all remaining slashes with a semicolon followed by space. We return the modified
string.
The Search tab 61
OK, let’s come back to explaining putResultsOnPage(). We create in defText a nicely formatted
definition line. We use String.replace() on this definition line to highlight the user’s search
term in our trademark red. For this we use global_searchTerm which we’ll define a bit later on.
buttonSpinnerVisible() is a simple CSS style toggler that also goes in search_interface.js
and looks like this:
//Toggle for search button's loading spinner
function buttonSpinnerVisible(visible)
{
var spinner = document.getElementById('button-spinner');
if(visible)
{
spinner.style.visibility = 'visible';
}
else
{
spinner.style.visibility = 'hidden';
}
}
Remember in websql_core.js we did this a couple of times:
document.getElementById('loading-text').innerHTML = '';
As this is manipulating the search interface, let’s refactor this as a function in search_-
interface.js. Let’s call it clearLoadingMessage():
function clearLoadingMessage()
{
document.getElementById('loading-text').innerHTML = '';
}
Then replace the two document.getElementById('loading-text').innerHTML = ''; lines in
websql_core.js with calls to clearLoadingMessage();.
OK, in search_interface.js we now have all of the functions that other functions might call
to update the interface for database searching, but we are missing something here. The user! We
need to catch tap events on the Search button and then use their entered query to search the
database and return results. Let’s start where the user starts - the Search button. Let’s add a click
handler. Add a call to:
configureSearchButton();
at the bottom of receivedEvent() in good old index.js.
We define configureSearchButton() in search_interface.js:
The Search tab 62
//search button clickability
function configureSearchButton()
{
document.getElementById('search-button').addEventListener('click', onclickForSearch
Button, false);
}
We define onclickForSearchButton() as the click handler for the search button.
onclickForSearchButton(), also in search_interface.js, is like this:
//Perform a dictionary search for entered query
function onclickForSearchButton(event)
{
var q = document.getElementById('search-query').value;
//some kanji searches are going to be legitimately only one char
if(q.length  1)
{
return;
}
buttonSpinnerVisible(true);
var matches = doEdictQueryOn(q);
}
We get the user’s entered search query and - on the condition that it’s at least one character long
- we pass it to doEdictQueryOn() after displaying the “searching” spinner. doEdictQueryOn() is a
function that we haven’t written yet that will need a whole ‘nother JavaScript file. We’ve already
got quite a few JavaScript files, but this is keeping it nice and modular. Create websql_query.js
in /assets/www/js and add doEdictQueryOn() thus:
//function to query the database based on whatever query string
function doEdictQueryOn(newQ)
{
//TODO
}
What? It’s empty! Yes, we’re going to take a breather now and plan what we’re going to do next.
A keyboard break if you like. Remember back in the Layout and interface section of this chapter
when we laid down some rules about our app? It’s time to recap those now as it will affect how
we implement dictionary searching. We said we’ll stick to a rule “that the user’s search query
can be in Japanese as well as English”. Obviously we’ll then go down different search query
routes depending on the entered language. So we need a way to detect if the query is Japanese
or English.
The Search tab 63
..
Why two search querying routes?
We could get away with not detecting the input query’s language by having this kind of logic:
“Assume the query is English, do a search, if no results then assume it’s Japanese
and search again”
Which has two problems. We have to make an assumption about how our app is being mostly
used. (Admittedly we could change the assumption if we find out it’s wrong.) Another problem
is performance - we may be searching unnecessarily.
A very simple, and linguistically incorrect!, way to do this is to see if we have multibyte
characters in our query string or not. We’ve set our HTML page to be UTF-8. UTF-8 is interesting
because it’s a flavour of Unicode that’s backwards compatible with good ol’ ASCII. ASCII can
be utf-8, but so can Japanese! But ASCII won’t set the right bits in each byte to be considered
a multibyte stream. Something we can use to our advantage is that for ASCII, the length of a
string in bytes will also be the length of that string in characters. For multibyte utf-8 strings, this
will not be the case and the byte length will be greater than the character length.
Let’s implement an is_mb() (“mb” meaning “multibyte”) check using this knowledge. Keeping
things modular, and realising that we are going to need functions soon for Japanese language
handling, make a new file in /assets/www/js called linguistics.js. Add is_mb() thus:
//Does the given utf8 string have multibyte characters or not?
function is_mb(utf8String)
{
return utf8String.length != mb_bytelen(utf8String);
}
Here we compare a string’s length in characters (using the length property - JavaScript operates
internally with utf-16 unicode) with its length in bytes. mb_bytelen() is the key function
here that we need to write. It will give us the byte length for a utf8 string. Put it also in
linguistics.js:
//Get length in BYTES of a utf8 string
function mb_bytelen(utf8String)
{
//Matches only the 10.. bytes that are non-initial characters
//in a multi-byte sequence.
var m = encodeURIComponent(utf8String).match(/%[89ABab]/g);
return utf8String.length + (m ? m.length : 0);
}
In utf-8, everything is a sequence of bytes. For an ASCII character, one byte is the full sequence
- that byte is the character. But it allows for multibyte characters by the initial byte in
The Search tab 64
that character’s sequence of bytes setting a special bit. This special bit tells the browser (or
programming language or text editor etc) that “there’s more to come!” and the browser adds
the remaining bytes in the sequence to get the full value for that character. The remaning bytes
also set a special bit so the browser knows when that particular character has all of its bytes read.
For the gory details please see http://en.wikipedia.org/wiki/UTF-8.
So what we are doing in mb_bytelen() is adding the length of the string in characters to the
count of non-initial character sequence bytes. This will give us the total byte length for any utf8
string - containing multibyte characters or not!
OK, is_mb() is one important tool for our database querying logic in the bag. Using it, let’s think
more about our query logic with some pseudocode:
if(is_mb(searchTerm)) {
//searchTerm is Japanese (or at least multibyte)
//
//[1] exact kanji match
//[2] exact kana match
} else {
//searchTerm is English or, as last resort, romaji
//
//[1] exact definition match
//[2] partial definition match
//[3] exact romaji match (on kana field)
}
So, if we detect multibyte characters in the search term, we assume it is Japanese and try to
match it exactly against, first, the kanji field of the words in our database. Then, if that produces
no results, we try to match it exactly against the kana field. This priority order is realistic because
kanji (Chinese idiogrammic characters) are the “correct” way to write a Japanese word. The kana
is just the way to pronounce those Chinese characters. Though note that some words are kana
only and don’t hava a kanji.
We assume that the search term is in English if it contains no multibyte characters. Then we
focus on the definition field of our database. Remember that definition entries look like this:
/uncertain/vague/ambiguous/
Multiple definitions are separated by slashes. So our most relevant results (query [1] of the
English route) would be to find the search term exactly as one of these definitions. A search
for “vague” would match the above definition, for example. If that produces no results, we query
for partial matches of the search term in these definitions. For example a search for “director”
will match a definition of /company director/board member/. Finally, if we still have no
results, we can take a gamble and assume that the user has entered a term in romaji (which
is Japanese written in abc like “sayonara” or “moshimoshi”). For this we’ll have to convert the
search term into phonetic kana and query for a matching kana field. So this one needs a bit more
work programmatically.
Note that if we go down the Japanese route, and get no results at the end, we don’t then proceed
down the English route (and vice-versa).
The Search tab 65
Let’s implement the Japanese route first as the queries are easier. We’ll go back to working in
websql_search.js. Remember from writing websql_core.js how Web SQL works with callback
chains? Well, with this in mind (and don’t get me wrong, there are better and cleverer ways to
do this) we’re going to stick a couple of global variables at the top of websql_search.js so that
all callback functions can access them:
//User's search term as a global variable (so we can access it from all the different c
allbacks). Hmmm...
var global_searchTerm = null;
//Maximum number of search results to return for any query
var global_maxResultsCount = 40;
Now make doEdictQueryOn() look like this:
function doEdictQueryOn(newQ)
{
//set global_searchTerm
global_searchTerm = newQ;
//version 1.0, 4 megabytes
var db = window.openDatabase(Japxlate, 1.0, Japxlate DB, 4 * 1024 * 1024);
if(is_mb(global_searchTerm)) //Japanese (or at least multibyte)
{
//console.log('doing as japanese - kanji');
db.transaction(queryDB_ja, errorWebSQL);
}
else //ie. English (or - as last resort - romaji)
{
console.log('doing as english - exact');
}
}
We simply save the search term into global_searchTerm, open the database and attempt the
queryDB_ja() query function (using our generic Web SQL error handler). We’ll come back to
the else section for English later, but in the meantime let’s make queryDB_ja() which is like
this:
The Search tab 66
//Search edict for an exact kanji match
function queryDB_ja(tx)
{
var safeQ = global_searchTerm;
//use placeholders (so we don't need to escape the query)
tx.executeSql(SELECT * FROM edict WHERE kanji = ? LIMIT  + global_maxResultsCount
, [safeQ], successQueryDB_ja, errorWebSQL);
}
We accept an SQLTransaction object - as per the Web SQL specification - and call it tx for short.
We use tx.executeSql() to run a very simple SQL query on the edict table; Matching the kanji
field exactly to the search term. Note how we get the search term from the global variable we
defined earlier.
In the SQL query, we have kanji = ?, the question mark is a placeholder for parameter (or
value) binding. We then specify the value to be bound to this placeholder in the 2nd argument to
tx.executeSql(), in this case safeQ. Why do this? Why not just query for kanji = ' + safeQ
+ ', ie. literally. Well because, this way, if the entered search term contains single quotes or
slashes or anything that Web SQL considers “special”, the SQL query will break and result in
errors. When we use parameter binding, Web SQL is going to escape the parameter value for
us so that we are safe from dodgy characters accidentally breaking our SQL (or malicious “SQL
injection” attacks). You can try this a little later if you like to see how wrong it can go!
So we query with a success callback of successQueryDB_ja() and our all-purpose error handler.
Note that successQueryDB_ja() will be called if the executeSql() query is valid and executes
- which means even if zero results are returned. So successQueryDB_ja() is going to look like
this:
//Callback for if queryDB_ja() did not error (which includes zero results)
//Print kanji matches if we have any ELSE try kana matches
function successQueryDB_ja(tx, results)
{
if(results.rows.length == 0) //no kanji matches - try kana matches
{
//console.log('no ja kanji matches');
//version 1.0, 4 megabytes
var db = window.openDatabase(Japxlate, 1.0, Japxlate DB, 4 * 1024 * 1024);
db.transaction(queryDB_ja_kana, errorWebSQL);
}
else
{
putResultsOnPage(results);
}
}
The Search tab 67
We receive the SQLTransaction and an SQLResultSet. We check the rows.length property of
the resultset to see if we got any matches or not. If we have no matches then we open the DB
again and run a different query function on it, namely queryDB_ja_kana() which is going to
search for kana matches against the search term. If we have any matches, we simply call our
putResultsOnPage() function (that we made previously in search_interface.js) and pass it
the resultset. Then we are done with this particular query route. OK, we still need to implement
queryDB_ja_kana(), which - yes you’ve guessed it - is almost identical to queryDB_ja() but
using the kana field:
//Search edict for an exact kana match
function queryDB_ja_kana(tx)
{
var safeQ = global_searchTerm;
//use placeholders (so we don't need to escape the query)
tx.executeSql(SELECT * FROM edict WHERE kana = ? LIMIT  + global_maxResultsCount,
[safeQ], successQueryDB_ja_kana, errorWebSQL);
}
We simply print out any and all results that we might have.
We are now ready to give this Japanese query route a test drive! First, include the new JavaScript
files we’ve made at the bottom of index.html so that it looks like this:
.
.
script type=text/javascript src=js/linguistics.js/script
script type=text/javascript src=js/search_interface.js/script
script type=text/javascript src=js/websql_edict_inserts.js/script
script type=text/javascript src=js/websql_core.js/script
script type=text/javascript src=js/websql_search.js/script
script type=text/javascript src=js/japxlate.js/script
script type=text/javascript src=js/index.js/script
script type=text/javascript
app.initialize();
/script
.
.
Run it! Enter an English search term and click the search button. You’ll get a console message of
“doing as english - exact”, and the spinner will start to spin and not stop! We’ve obviously not
finished the English query route yet.
Let’s check if it is really searching for any entered Japanese terms. Go ahead and copy some
random text from http://www.yahoo.co.jp and paste it into our app’s search box. Click search.
You’ll probably get the “no matches found” message (unless you got really lucky!). OK, so that’s
working. What about an actual match? Open up the websql_edict_inserts.js file and copy any
The Search tab 68
kanji or kana INSERT value. Search for this on the app and you should get the corresponding
definition. Nice!
This is pretty awesome right now. It’s beginning to feel like a useful, working app! OK, before
the very final thing we need to implement for searching (I’ll let you guess what you think it is
;-)) let’s tackle that English searching route. Go back to the else clause in doEdictQueryOn() (in
websql_search.js) and edit it to actually do something:
.
.
if(is_mb(global_searchTerm)) //Japanese (or at least multibyte)
{
//console.log('doing as japanese - kanji');
db.transaction(queryDB_ja, errorWebSQL);
}
else //ie. English (or - as last resort - romaji)
{
console.log('doing as english - exact');
db.transaction(queryDB_en, errorWebSQL);
}
.
.
We do what we do for Japanese just with a different query function called queryDB_en() which
is going to do step [1] of our English route and is like this:
//Search edict for an exact English match
function queryDB_en(tx)
{
var safeQ = global_searchTerm;
//use placeholders (so we don't need to escape the query)
tx.executeSql(SELECT * FROM edict WHERE definition LIKE ? LIMIT  + global_maxResu
ltsCount, ['%/' + safeQ + '/%'], successQueryDB_en, errorWebSQL);
}
Here we query the definition field of our edict table and note how we use the LIKE operator
and not, as with the Japanese route queries, the ‘=’ operator. LIKE allows us to use wildcard
characters which allows us to do a fuzzier search. We need that functionality here as we are
trying to match only one of each database row’s many definitions (separated by ‘/’).
What’s going on with our 2nd argument where we have to specify the value for parameter
binding? Well, we basically build a LIKE condition that will match, completely, safeQ as ANY
one of the definition entries - first one, last one or any of the middle ones. ‘%’ is the SQL wildcard
meaning “match anything” and actually it will match zero characters if applicable too! With a
definition of:
/one/two/three/
The Search tab 69
then the same format of condition like: ‘%/one/%’, ‘%/two/%’, ‘%/three/%’ will match each corre-
sponding definition respectively. We simply build this pattern and put it in the 2nd argument.
We use the general error handler again and the success handler is successQueryDB_en():
//Callback for if queryDB_en() did not error (which includes zero results)
//Print exact matches if we have any ELSE try partial matches
function successQueryDB_en(tx, results)
{
if(results.rows.length == 0) //no exact matches - try partial matches
{
//console.log('no en exact matches');
//version 1.0, 4 megabytes
var db = window.openDatabase(Japxlate, 1.0, Japxlate DB, 4 * 1024 * 1024);
db.transaction(queryDB_en_partial, errorWebSQL);
}
else
{
putResultsOnPage(results);
}
}
This is cut from the same mould as successQueryDB_en() that we’ve just done. If we have
no results from exact matching, we move on to step [2] which is partial matches by calling
queryDB_en_partial():
//Search edict for a partial English match
function queryDB_en_partial(tx)
{
var safeQ = global_searchTerm;
//use placeholders (so we don't need to escape the query)
tx.executeSql(SELECT * FROM edict WHERE definition LIKE ? LIMIT  + global_maxResu
ltsCount, ['%' + safeQ + '%'], successQueryDB_en_partial, errorWebSQL);
}
This is very very similar to queryDB_en(), but the important difference is in the LIKE condition.
We do not use slashes here which means we are not locked down to an exact match and will
match any definition list where the user’s search term appears. For example, searching for “user
interface” will match a definiton of:
/graphical user interface/GUI/
The success callback here is successQueryDB_en_partial() which is going to trigger the final
step [3] of English searching, or display results from this step [2].
It is in the same shape as the other success callbacks so far:
The Search tab 70
//Callback for if queryDB_en_partial() did not error (which includes zero results)
//Print partial matches if we have any ELSE try romaji matches
function successQueryDB_en_partial(tx, results)
{
if(results.rows.length == 0) //no partial matches - try as romaji
{
//console.log('no en partial matches');
//version 1.0, 4 megabytes
var db = window.openDatabase(Japxlate, 1.0, Japxlate DB, 4 * 1024 * 1024);
db.transaction(queryDB_en_romaji, errorWebSQL);
}
else
{
putResultsOnPage(results);
}
}
We do step [3] - if we need to - by calling queryDB_en_romaji(). This is going to be the fiddly step
that we mentioned earlier as it will need to convert search terms like “sayonara” or “moshimoshi”
into phonetic Japanese kana so we can then search the database. queryDB_en_romaji() is like
this:
//Search edict for a romaji match
function queryDB_en_romaji(tx)
{
var safeQ = global_searchTerm;
var safeQKana = romaji_to_hira(global_searchTerm);
//use placeholders (so we don't need to escape the query)
tx.executeSql(SELECT * FROM edict WHERE kana LIKE ? LIMIT  + global_maxResultsCou
nt, [safeQKana], successQueryDB_en_romaji, errorWebSQL);
}
We convert the search term into hiragana (which is one of the Japanese phonetic scripts and the
most common one used in the kana field of our table) via romaji_to_hira() which we implement
very soon. The query is straightforward, but don’t forget to implement the success callback of
successQueryDB_en_romaji() which is a carbon copy of successQueryDB_ja_kana() but with
a different name.
So we’ve come to a bit of a dead-end as we need to implement the romaji_to_hira() script
conversion function. Well, I know from the experience of building the @japxlate bot - and
Mapanese - that we can cover almost all cases of Japanese – English script conversion by
simple string replacement operations. For example, we have a table of all Japanese characters
and then a corresponding table of English spellings for those characters. Then we can convert
Japanese script to English and vice versa.
The Search tab 71
JavaScript has a builtin String.replace() method, but it works by replacing the first (or all)
matching regexes in the string with the supplied replacement value. We can’t give it a list of
targets and a list of corresponding replacements. We want something a little easier to use, and
so we’re going to go deep down and dirty with some advanced JavaScript. We are going to
prototype a new method onto the String object which means we can add a new method to the
String class ONCE and it is available to any variable of type string in JavaScript! Let’s put this in
linguistics.js (we’ll get back to database querying when we’ve got the language conversion
all done and dusted). OK, code first explanations second:
//Here we use prototyping to add a method to the String class to give
//us the equivalent of PHP's str_replace()
String.prototype.str_replace = function(find, replace)
{
var replaceString = this;
var regex;
for (var i = 0; i  find.length; i++) {
regex = new RegExp(find[i], g);
replaceString = replaceString.replace(regex, replace[i]);
}
return replaceString;
};
‘String’ is JavaScript’s object name for character strings. Any variable - or literal - that’s a string
will be of object type ‘String’. That’s how we can run .replace() and .match() and things
like that on any JavaScript string - because they are all String objects and the String object has
prototypes of those methods.
So the syntax to prototype a new method into the String object is:
String.prototype.newMethodName = function(any, args, you, need){code; to; do; stuff;};
We name the method “str_replace” (in honour of PHP ;-)) and define it as a function accepting
two parameters; find and replace - both of which are character arrays.
In a prototype method, the context of ‘this’ will refer to the object on which the method was
called. For example, if calling myStringVariable.str_replace(), then in the str_replace()
protoype, ‘this’ will be myStringVariable.
We save the string in replaceString. We then loop over each item in the find array and globally
(the ‘g’ modifier) replace any occurrences of it with the corresponding character in the replace
array. So yes, the find and replace arrays need to have the same number of items in them which
we don’t explicitly police here.
Before we write romaji_to_hira(), we need the character tables that our String.str_-
replace() will operate on. I won’t dwell on these too much, and it’s best to simply paste these
in to your code as a black box - this isn’t a linguistics course! Though the variable names and
comments will help if you want to read through it. Stick these at the top of linguistics.js:
The Search tab 72
//----character tables----------------------------------------------------------
//All single character hiragana (in biggest first order)
var coreHiragana =
[
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '',
'', '', '', '', '',
'', '', '', '',
'', '',
'', '', '', '', '',
'', '', '',
'', '', '', '', '',
];
//All single character katakana (in biggest first order)
var coreKatakana =
[
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '', '', '',
'', '', '',
'', '', '', '', '',
'', '', '', '',
'', '',
'', '', '', '', '',
'', '', '',
'', '', '', '', '',
];
//Transliterations of coreHiragana
The Search tab 73
var coreRomaji =
[
'ga', 'gi', 'gu', 'ge', 'go',
'za', 'ji', 'zu', 'ze', 'zo',
'da', 'di', 'du', 'de', 'do',
'ba', 'bi', 'bu', 'be', 'bo',
'pa', 'pi', 'pu', 'pe', 'po',
'ka', 'ki', 'ku', 'ke', 'ko',
'sa', 'shi', 'su', 'se', 'so',
'ta', 'chi', 'tsu', 'te', 'to',
'na', 'ni', 'nu', 'ne', 'no',
'ha', 'hi', 'fu', 'he', 'ho',
'ma', 'mi', 'mu', 'me', 'mo',
'ya', 'yu', 'yo',
'ra', 'ri', 'ru', 're', 'ro',
'wa', 'wi', 'we', 'wo',
'n', '', //preserve chiisai tsu
'a', 'i', 'u', 'e', 'o',
'ya', 'yu', 'yo',
'a', 'i', 'u', 'e', 'o',
];
//All combination katakana
var comboKatakana =
[
'', '', '', '',
'', '', '', '',
'', '', '', '',
'', '', '',
'', '', '',
'', '',
'', '', '',
'', '', '',
'', '', '',
'', '', '',
'', '', '',
'', '', '',
'', '', '', '',
'', '', '',
'', '', '', '',
'',
''
];
//Transliterations of comboKatakana
var comboRomaji =
[
'cha', 'chu', 'che', 'cho',
'sha', 'shu', 'she', 'sho',
The Search tab 74
'ja', 'ju', 'je', 'jo',
'kya', 'kyu', 'kyo',
'gya', 'gyu', 'gyo',
'ryu', 'ryo',
'mya', 'myu', 'myo',
'hya', 'hyu', 'hyo',
'nya', 'nyu', 'nyo',
'bya', 'byu', 'byo',
'pya', 'pyu', 'pyo',
'dya', 'dyu', 'dyo',
'fa', 'fi', 'fe', 'fo',
'wi', 'we', 'wo',
'va', 'vi', 've', 'vo',
'ti',
'di'
];
//----/end character tables-----------------------------------------------------
The “combo” tables represent larger Japanese phonics that are written with two characters. We
need to search and replace these first in order to prevent splitting any of them up by searching
and replacing single characters first.
Note that we don’t define a “comboHiragana” table because we can get that by computing
comboKatakana.str_replace(coreKatakana, coreHiragana); if we need to.
romaji_to_hira() is going to now look like this:
//Convert romaji to hiragana
function romaji_to_hira(romajiString)
{
//replace combos first
var katakana = romajiString
.str_replace(comboRomaji, comboKatakana)
.str_replace(coreRomaji, coreKatakana);
//force hiragana
return kata_to_hira(katakana);
}
We accept a string in romaji (abc) and then run String.str_replace() on it twice using a
technique called chaining. We replace combo characters first and then single characters. We
now have a converted string in katakana, but as the function name implies we want to return
hiragana. We return the katakana as modified by kata_to_hira() which we implement, again
in linguistics.js, thus:
The Search tab 75
//Convert katakana to hiragana
function kata_to_hira(katakanaString)
{
return katakanaString.str_replace(coreKatakana, coreHiragana);
}
Here we simply replace all katakana with the corresponding hiragana. We don’t need to bother
with combo characters here as this will cover all cases.
The English search route is ready to go! Give it a whirl by searching for some words and seeing
what - if any - results you get. To be double dog sure that we are trying exact definition matches
first and then falling back to partial matches, have a peek at the websql_edict_inserts.sql file
again and pick out some definitions to searh for.
In fact, we’ve not had any screenshots of the app for a while so let’s have one for each type of
search (kanji, kana, English exact, English partial):
Figure 31. Respective results for kanji, kana, English (exact matches found) and English (partial matches found)
queries
Great! Though looking at these reminds us that we still need to, in putResultsOnPage() of
search_interface.js, somehow convert the kana field from the database into romaji to make
our result lines easier to understand. In that function, change this bit:
var theRomaji = item.kana; //TODO
to this:
var theRomaji = kana_to_romaji(item.kana);
Let’s implement kana_to_romaji() in linguistics.js and it will be somewhat the opposite of
our current romaji_to_hira(). kana_to_romaji() is like this:
The Search tab 76
//Convert kana (hira or kata) to romaji
function kana_to_romaji(kanaString)
{
//force katakana
var kata = hira_to_kata(kanaString);
//transliterate
var withChiisaiTsu = kata.str_replace(comboKatakana, comboRomaji)
.str_replace(coreKatakana, coreRomaji);
//fix any remaining chiisai tsu's
//before 'chi' (make tchi)
var romaji = withChiisaiTsu.replace(/chi/g, 'tchi');
//before anything else (double the consonant)
romaji = romaji.replace(/([a-z]{1})/g, $1$1);
//TODO katakana style '' (which might actually be '-' in the input string)
romaji = romaji.replace(/([^0-9])[-]([^0-9])/g, $1$2);
romaji = romaji.replace(/([a-z]{1})/g, $1$1);
return romaji;
}
Again it’s best to think of this as a black box, but what it’s doing is the opposite of romaji_-
to_hira() but with some extra cleanup steps at the end. Searching for anything now has romaji
(abc) in brackets on each result line:
The Search tab 77
Figure 32. We now get each result word spelled out in abc (romaji)
Sweet!
Before scrolling - which will be epic - there’s just one more niggle. You might have noticed
so far that we can get search results by clicking the search button, but not by pressing enter (or
equivalent) on the on-screen keyboard after we’ve typed the search term. Correctly implemented
HTML forms will let you press enter in a text input field to submit the form. It will perform the
same as clicking the form’s submit button. We don’t technically have a form here - as we
aren’t submitting to a remote server, it’s all client-side - but we should emulate this behaviour
because:
• Other apps do it
• It is expected UX and is “normal”
It is surprisingly easy to implement, we simply need a handler for keypress events on the search
input. This event fires every time a character is typed and then inserted into a text input or
textarea etc. The event will tell us which key was pressed and we simply need to treat the ENTER
key as a special case because we then want to do some processing (and not put a character into
the text field).
Stick a:
configureSearchInput();
at the bottom of receivedEvent() in index.js. Define this function in search_interface.js
thus:
The Search tab 78
//search box ENTER keypress
function configureSearchInput()
{
document.getElementById('search-query').addEventListener('keypress', onkeyForSearch
Input, false);
}
We set onkeyForSearchInput() as the keypress handler for our search text input.
onkeyForSearchInput() also lives in search_interface.js and is this:
//Simulate a normal HTML form input by allowing an ENTER press in the
//query input to perform the same as clicking the search button
function onkeyForSearchInput(event)
{
//.charCode or .keyCode ??
if(event.keyCode == 13) //ENTER key
{
//trigger the already registered click handler
document.getElementById('search-button').click();
}
}
We simply use the keyCode property of the received event to detect an ENTER press and
then call the already registered click handler for the search button. For anything other than
ENTER, we “do nothing”. How we call the click handler manually is worth talking about. We
simply get the relevant DOM element using document.getElementById() (or you could use
document.querySelector() or what-have-you) and then call .click() on it. This will trigger
the registered click handler for that element.
You’ll be wondering now “but our click handler for the search button receives a mouse event - a
click in fact. What will it receive in this case?” Interestingly, after manually calling .click() on
an element, that element’s click handler will be triggered with a dummy mouse event (where, for
example, the x and y coordinates are zero and etc). Depending on what you do with the mouse
event in the click handler, it may or may not make sense to call it manually with .click(). In
our case, the click handler doesn’t even use the received event, and so we are fine.
Give it a whirl! You can now search by hitting the ENTER (or equivalent) key after typing a
search term. It will work on desktop Chrome or your device! Sweet!
8.4 Results scrolling
The final epic thing on our Search tab is results scrolling. You’ve probably already noticed that
if your search produces lots of results, it is simply clipped at the bottom of the screen (actually
just above our footer). If you haven’t noticed this yet, then try searching for “it’s” and you’ll see
the problem.
The Search tab 79
With desktop Chrome, you can scroll as per normal with the scrollbar that appears - or your
mouse wheel. In fact, the search box and search button also scroll because this is the japxlate_-
app div that is scrolling, due to CSS overflow:auto;
So why don’t we have scrollbars or scrollability on the device? It’s because the Android WebView
browser (and the stock browser app) will only allow scrolling when the entire html document
itself is larger than the viewport. Even then it doesn’t show scrollbars. It would be very very
fiddly on a small mobile screen if, say, the html document itself was scrollable and then a small
div inside of that was scrollable too! This is why CSS scrolling does not work on WebView.
What we need to do is to use browser events to implement our own scrolling for search results.
Also, let’s limit scrollability to our #results-wrapper div so that we don’t scroll the search box
and button.
So, we probably want to detect a finger drag on the results and then scroll based on that. And
we’ve just seen that the DOM event of “click” (on the search button) worked for both mouse
clicks and finger taps. So, to detect a finger drag on our device we can probably just detect a
“mousemove” event or something like that huh? Annoyingly no. The “traditional” DOM mouse
events of “mousedown”, “mousemove” and “mouseup” do NOT get triggered in WebView when
putting a finger down, moving and then releasing the finger. This is initially very annoying and
confusing, but it makes sense really because of things like multi-finger gestures which obviously
have no parallel on a mouse. Maybe in the future there will even be pressure sensitive mobile
screens?
The events in question are touchstart, touchmove and touchend. These somewhat correlate to the
mousedown, mousemove, and mouseup events. Remembering that the parent #results-wrapper
div is actually our static “window” on the search results, we need to attach scrolling behaviour
to #search-results which is where the search result content gets written to.
Mosey on back to receivedEvent() in index.js and stick a call to:
configureSearchTouchScrolling();
at the bottom. And yes, you’ve guessed it, we are going to define this function in search_-
interface.js . thus:
//configure touch dragging for search results
function configureSearchTouchScrolling()
{
document.getElementById('search-results')
.addEventListener('touchstart', touchstartForSearchResults, false);
document.getElementById('search-results')
.addEventListener('touchmove', touchmoveForSearchResults, false);
document.getElementById('search-results')
.addEventListener('touchend', touchendForSearchResults, false);
}
We simply register one custom handler function for each of the touch events. To get the ball
rolling, define placeholders for these handlers - still in search_interface.js - thus:
The Search tab 80
//Touchstart event handler for search results div - initiates touch scrolling
function touchstartForSearchResults(event)
{
console.log('touchstart');
}
//Touchmove event handler for search results div - performs touch scrolling
function touchmoveForSearchResults(event)
{
console.log('touchmove');
}
//Touchend event handler for search results div
function touchendForSearchResults(event)
{
console.log('touchend');
}
This is now runnable but note that it won’t do anything on your desktop Chrome as nothing
can trigger touch events! So run this on your device, search for “it’s” (a good test as it matches a
lot of entries) and then drag your finger up and down over the results. Your Eclipse LogCat will
show something like this:
Figure 33. Touch events captured in LogCat
Nice! Of interest is that if you tap the results, you’ll trigger a touchstart immediately followed
by a touchend. ie. there will be no “move”.
Cool, so we are already catching the events that we need for scrolling, we just need to scroll!
What we’ll do is we’ll get the y (or vertical) coordinate of wherever the finger was moved to, and
use that to change the CSS top property of #search-results accordingly. Remember that we set
#search-results to position:relative; which means that we can set its “top” property to any
value (in pixels) that we like. A negative top will move the results up and a positive top will move
the results down. Essentially, if a finger touches at y=60 and then moves up to y=30 (a lower y is
The Search tab 81
higher up the screen) we know that we should move the results div up by 30; which is to say a
top value of -30px.
OK, we’ve already got three different touch events each with its own handler function. I’m
thinking already that we are going to need some evil global variables to store things that will be
shared between these handlers. For example, finger y positions and so on. Stick these at the top
of search_interface.js:
//start y axis position (in pixels) of the current scroll
var global_scrollStartY;
//current 'top' css value (in pixels) of our scrollable div
var global_scrollDivTop;
//height (in pixels) of our viewport over the scrollable div (used to activate scrollin
g)
var global_scrollWindowHeight;
//current height (in pixels) of our scrollable div's content (used to activate scrollin
g and for scroll locking)
var global_scrollDivHeight;
For every finger scroll we want to know the start y of the results div and the start y of the finger.
Then we can find out how far up (or down) the finger moves and simply subtract (or add) this
to the top value of the results div. We also need to know (a) do we need scrolling at all? and
(b) when to stop scrolling to prevent content being scrolled off the viewport! For both (a) and
(b) we save the height of the scrollable content and the height of the scroll viewport. We saw
earlier from fiddling with the device screen and looking at LogCat that finger scrolling is split
into three steps; touchstart, touchmove then touchend. We’ll map these three different events to
three different steps for our scrolling. Thus:
• touchstart ⇒ finger scrolling may start
• touchmove ⇒ finger scrolling happening now!
• touchend ⇒ finger scrolling (if it was happening at all) has stopped
Sidenote now, but have you noticed that we are no longer able to debug results scrolling in
desktop Chrome? To get rid of this annoyance, we’ll implement our touch scrolling in as generic
a way as possible so that we can - a little bit later in the tutorial - add simulated touch scrolling
by using mouse events instead of touch events.
OK, go back to touchstartForSearchResults() and touchmoveForSearchResults() and make
them look a bit like this:
The Search tab 82
//Touchstart event handler for search results div - initiates touch scrolling
function touchstartForSearchResults(event)
{
//console.log('touchstart');
touchobj = event.changedTouches[0]; //reference *first* touch point
startVerticalDragScrolling(this, touchobj.clientY);
event.preventDefault(); //prevent default tap behavior
}
//Touchmove event handler for search results div - performs touch scrolling
function touchmoveForSearchResults(event)
{
//console.log('touchmove');
touchobj = event.changedTouches[0]; //reference first touch point for this event
doVerticalDragScrolling(this, touchobj.clientY);
event.preventDefault();
}
Well we don’t do much in these handler functions themselves, other than call soon-to-be-written
helper functions and then preventing the default action for the touch event in question. We
bundle away scroller functionality into helper functions to keep things nice and generic which
will help us later when we go back and get scrolling working with the mouse. As the default
behaviour for dragging a finger over some text would be to select that text, we prevent this
default.
The key point here is how to use the touch event that we receive. Touch events contain a
changedTouches property which is an array of touch objects. Each touch object in the array
represents a single touch directly involved in this event. Which for touchstart means all the
fingers that hit the screen, and for touchmove means all the fingers that moved.
As we don’t need or want to do anything fancy with multi touch gestures on Japxlate, we can
simply access .changedTouches[0] and ignore the rest. There will always be at least one touch
object in changedTouches[0], and there may or may not be more.
We pass the clientY property of our touch object to our helper functions. As the first argument,
we also pass ‘this’, which if you remember for event handler functions means the element that
the event triggered on - in this case the search results div.
See http://www.javascriptkit.com/javatutors/touchevents.shtml for more about touch events in
JavaScript.
The Search tab 83
..
NOTETOSELF SIDENOTE about the different JavaScript event coordinate systems [dont 4get
that for mobile there is one extra which is the current poz of the small device window on the
bigger client window
OK, so the meat-and-bones of scrolling are bundled away in helper functions. Let’s have a look
at our scroll initiator - startVerticalDragScrolling() - first of all:
//initialise vertical scrolling for ontouchmove
function startVerticalDragScrolling(elementToScroll, eventClientY)
{
//console.log('initialise scrolling');
var theStyle = window.getComputedStyle(elementToScroll);
global_scrollDivTop = parseInt(theStyle.top); //get 'top' value of box
global_scrollStartY = parseInt(eventClientY); // get x coord of touch point
global_scrollDivHeight = parseInt(theStyle.height); //get 'height' value of box
//work out height of #search-results versus height of results
//pane (which is .japxlate_app.height - #search-form.height)
global_scrollWindowHeight =
parseInt(
window.getComputedStyle(
document.querySelector('.japxlate_app')
).height, 10) -
parseInt(
window.getComputedStyle(
document.querySelector('#search-form')
).height);
}
So we expect to receive an elementToScroll which could be any old element (but with the right
CSS settings) but in our case will be the search-results div. We also expect an eventClientY value.
All we do in this function is save elementToScroll’s CSS top value, and eventClientY to the global
variables we defined earlier on. The novelty here is the use of window.getComputedStyle()
which will return the CSS style properties of the specified element, but not the developer defined
style as per a CSS stylesheet rule or an inline style=something attribute. Rather this will return
the CSS properties that the browser’s rendering engine has given to the element to display it
where it is. This method is useful to get natural CSS values for elements that we haven’t styled
ourselves very aggressively - or at all.
We also save the height of the results div (global_scrollDivHeight) and the height of the results
pane. (The results pane being all the space in .japxlate_app div under the search form). We do
The Search tab 84
this so we can work out if we actually need to scroll at all! Note that to get the height of the
results pane, we subtract the height of the search form from the total height of .japxlate_app. We
get the height of the search form by getting the height of the #search-form wrapper div which
we need to implement in index.html thus:
div id=search class=current
div id=search-form
button type=button id=search-button style=float:right; width:45%; margin-
right:1%;
img src=img/search.png
Search
img id=button-spinner src=img/spinner.gif style=visibility:hidden;
/button
input type=text id=search-query placeholder=Japanese or English size=40
style=width:45%; margin-left:1%;
br
span id=loading-text
[Loading core dictionary. This takes a while the first time.
img src=img/spinner.gif]
/span
/div
div id=results-wrapper
.
.
/div
.
.
/div
That’s the initiator, now on the the actual scroller which is, of course, going to be a bit more
complex. We need to use the global values we just saved to work out how far we’ve scrolled and
then move the results div accordingly. We also should check if we need to do any scrolling at all
- there might be no overflow of content!
A first bash looks like this:
//do vertical scrolling for ontouchmove
function doVerticalDragScrolling(elementToScroll, eventClientY)
{
//console.log('do scrolling');
//if height of results content is less than height of results pane,
//we have no content overflow and so don't need to scroll
if(global_scrollDivHeight  global_scrollWindowHeight)
{
console.log('no overflow');
return;
}
The Search tab 85
//calculate distance travelled by touch point
var distance = parseInt(eventClientY) - global_scrollStartY;
//new CSS top for elementToScroll
var newTop = global_scrollDivTop + distance;
//set the new top value for the div we are moving
elementToScroll.style.top = newTop + 'px';
}
First and foremost, we return immediately if we see that we don’t need to do any scrolling
because the results content div is shorter than the results pane. This prevents the user from
being able to scroll a single result up and down the pane! Next, we have to work out how far
away we are from the touch start point. This distance becomes the amount we have to add to
- or subtract from - the top value of the results div. We access the CSS top value directly with
elementToScroll.style.top.
This works! Run it on you device! Nice! Just one problem, which we can see with these
screenshots:
Figure 34. We can scroll content past the top (left) and bottom (right) of the scroll viewport
Currently, we can scroll the content too far in either direction. We can fix this by editing
doVerticalDragScrolling() to look like this:
//do vertical scrolling for ontouchmove
function doVerticalDragScrolling(elementToScroll, eventClientY)
{
//console.log('do scrolling');
//if height of results content is less than height of results pane,
//we have no content overflow and so don't need to scroll
if(global_scrollDivHeight  global_scrollWindowHeight)
{
console.log('no overflow');
The Search tab 86
return;
}
//calculate distance travelled by touch point
var distance = parseInt(eventClientY) - global_scrollStartY;
//new CSS top for elementToScroll
var newTop = global_scrollDivTop + distance;
//disallow scrolling bottom of content higher than bottom of results pane
//(using height of results pane)
if(newTop  ((0 - global_scrollDivHeight) + global_scrollWindowHeight))
{
console.log('top cushion');
return; //return false??
}
//disallow scrolling top of content lower than top of results pane
if(newTop  0)
{
console.log('bottom cushion');
return; //return false?
}
//set the new top value for the div we are moving
elementToScroll.style.top = newTop + 'px';
}
(The changes are the two if clauses before the final elementToScroll.style.top.) To stop the
top of the results going lower than the top of the results pane, we simply prevent the results
div’s top property from going higher than zero. To prevent the bottom of the results from going
higher than the bottom of the results pane, we have to prevent the top value from going less than
negative(results height + results pane height). If the height of the results is 1000 pixels, and we
set the results div top to -1000 pixels, this will put the bottom of the results right at the top of
the results pane. From this state, adding, to top, the height of the results div will put the bottom
of the results at the bottom of the results pane. This is the minimum height we enforce here.
[NOTETOSELF the height of the results div.(?)]
Run this on your device and you’ll see that scrolling is “locked” and behaves more like native
Android.
Great, just one more problem which you might already be thinking about. Run the app and search
for “it’s” which produces lots of results. Scroll right to the bottom of the results. No problems
there. But then do a search that only gives a few results like “gas”. Eh? Where are the results?
Well, when you scrolled to the bottom of the results for “it’s” you moved the top value of the
search results div to quite a high negative number. This means that the top of the div is very
high up on the page, probably higher that the top of the screen! When you search again, the
div is still up there and a short amount of content will be obscured and not “reach down” to
the visible results pane. Clearly we need to reset the search result div’s top on every display of
The Search tab 87
search results. We can do this quite easily by a simple addition to putResultsOnPage() in our
search_interface.js file. Edit the start of this function to look like:
function putResultsOnPage(results)
{
//get search results div
var theDiv = document.getElementById('search-results');
//clear current content
theDiv.innerHTML = '';
//reset Y position because it might have changed after some touch scrolling frenzy!
theDiv.style.top = '0';
.
.
}
The only change here is the new line setting style.top to zero. This is all we need to fix the
scroll problem we’ve just experienced. Try it!
Money in the bank! Searching and scrolling is operational now and we are ready to move on
to the next tab. But do you remember we talked about, for debugging purposes, simulating the
touch scrolling with mouse events for desktop Chrome? Here’s a whistle-stop tour on getting
that working:
At the top of search_interface.js put:
var global_mouseButtonDown = false;
At the bottom of receivedEvent() in index.js put:
configureSearchMouseScrolling();
In search_results.js add:
//configure mouse dragging for search results
function configureSearchMouseScrolling()
{
//simulated touch (ie. mouse) dragging for results
document.getElementById('search-results')
.addEventListener('mousedown', mousedownForSearchResults, false);
document.getElementById('search-results')
.addEventListener('mousemove', mousemoveForSearchResults, false);
document.getElementById('search-results')
.addEventListener('mouseup', mouseupForSearchResults, false);
}
In search_interface.js add:
The Search tab 88
//Mousedown event handler for search results div - initiates simulated touch scrolling
function mousedownForSearchResults(event)
{
//console.log('mousedown event on scrollable');
global_mouseButtonDown = true; //set global
startVerticalDragScrolling(this, event.clientY);
event.preventDefault(); //prevent default click behaviour (ie. select text or whate
ver)
}
//Mousemove event handler for search results div - performs simulated touch scrolling
function mousemoveForSearchResults(event)
{
//console.log('mousemove event on scrollable');
if(!global_mouseButtonDown)
{
return false; //do nothing if the mouse button isn't pressed down
//false is ok to return?
}
doVerticalDragScrolling(this, event.clientY);
event.preventDefault();
}
//Mouseup event handler for search results div
function mouseupForSearchResults(event)
{
//console.log('mouseup event on scrollable');
global_mouseButtonDown = false;
event.preventDefault(); //need?
}
We simply recycle our existing scrolling helpers. The biggest difference is we need to track if
the mouse button is down or not as we don’t want to scroll on a mousemove when the mouse
button isn’t down.
[NOTETOSELF mention using libraries for mobile touch scrolling etc]
[NOTETOSELF and the android webkit hack that you found]
The Search tab 89
8.5 Extra credit challenges
Solutions not provided. Try to add:
1. “Content has become scrollable” indicator
2. “Can’t scroll anymore” indicator (the “flare” that native Android scrolling
usually has)
3. Our scrolling lacks the slippy, momentous feel that native Android scrolling
usually has. Try to add this (this will be very challenging!).
9. The Discover tab
9.1 Layout and interface
Now we move on to our second tab, Discover. This is going to be much simpler than the previous
tab so don’t worry! This chapter is a bit of a breather before we move on to the more complex
Write tab.
The discover tab is simply going to be a passive list of the latest Japanese words as tweeted by
the @japxlate bot. There will be no interactivity.
Note that, to speed up development and debugging, we can temporarily set the Discover tab to
be the app’s default tab. This saves you having to tap on the tab every time you run the app when
following this chapter. Simply move the class=currents off the Search tab and content div
and onto the Discover tab and content div.
Conveniently, anyone with a Twitter account can go into Settings ⇒ Widgets and create
embeddable timeline widgets - of their own feed or anyone else’s - based on User timeline,
Favourites, List or Search.
I’ve set up a User timeline widget under the actual @japxlate account using these settings:
Figure 35. Creating a User timeline widget on Twitter
Note that in order to show only our tweets out (ie. word definitions) we exclude replies and
we do not auto-expand photos. Note also that the widget must have a height in pixels - either
the Twitter default or your specification. After creating the widget, we get the similar-looking
Configuration page:
The Discover tab 91
Figure 36. Configuring a User timeline widget on Twitter
This tells us that we can embed the widget anywhere we want by using this code snippet:
a class=twitter-timeline href=https://twitter.com/japxlate data-widget-id=3786306
91635728384Tweets by @japxlate/a
script!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.loc
ation)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p
+://platform.twitter.com/widgets.js;fjs.parentNode.insertBefore(js,fjs);}}(document,
script,twitter-wjs);/script
Which is a stylised a element followed by some arcane looking JavaScript which actually
creates, programatically, a script tag with the appropriate JavaScript from Twitter to turn the
a element into the correct widget. Clever!
Let’s go ahead and stick this in the HTML for our Discover tab and see what happens. Make the
Discover tab in index.html look like this:
div id=discover class=current
a class=twitter-timeline href=https://twitter.com/japxlate data-widget-id=378
630691635728384
Tweets by @japxlate (network connection required) img src=img/spinner.gif
/a
script!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d
.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.s
rc=p+://platform.twitter.com/widgets.js;fjs.parentNode.insertBefore(js,fjs);}}(docume
nt,script,twitter-wjs);/script
/div
The Discover tab 92
(Note here we have made Discover the default content div as mentioned before - make sure to
also set the Discover tab too.)
We’ve changed the a inner text a bit. This text shows before the widget has loaded.
Running this looks like:
Figure 37. Embed of default Twitter User timeline widget
Which is pretty much a disaster! We’ve got a header and footer to the widget that doesn’t
make sense in this read-only context - we want just the tweets remember? We’ve also got a
scrollbar due to overflowing content - again we don’t want this. Digging deeper into the Twitter
documentation, we see from https://dev.twitter.com/docs/embedded-timelines#customization
that adding:
data-chrome=noheader nofooter
to the a tag will remove the header and footer. (“Chrome” here means the framing and em-
bellishment of the widget - not Chrome browser!) Note that the noscrollbar option mentioned
in the above Twitter documentation will only visually remove the scrollbar, scrollability is still
present and so we will instead remove scrollbars and scrollability with CSS techniques soon.
Add data-chrome=noheader nofooter to the a and running it looks like this:
The Discover tab 93
Figure 38. Embed of headerless and footerless Twitter User timeline widget
The widget header and footer have indeed gone, but there is still the scrolling issue. Also, the
widget is rather narrow and doesn’t take up the full width of the screen. Debugging in desktop
Chrome we can use the “inspect element” tool which is the magnifying glass at the bottom of
the F12 console. This tells us that the a is replaced with an iframe. An iframe is, simplistically,
like an embedded browser window in your web page. This will have ramifications for our app
which we mention later on.
The above Twitter documentation for timelines says:
“Setting a width is not required, and by default the widget will shrink to the width
of its parent element in the page.”
Which implies that if we put the a in a parent div of width:100%, then the widget will fill the
width of the screen. Let’s try it. Put the a and script in a parent tag thus:
div id=twitter-iframe-container
a.../a
script.../script
/div
Then style this parent div in index.css like this:
#twitter-iframe-container {
position:absolute; /*can now position relative to .japxlate_app which is*/
top:0; /*this div's first non-static parent*/
bottom:0;
width:100%;
overflow:hidden; /*clip overflowing content*/
}
A position:absolute element can be positioned relative to its first non-static parent (static being
position:static or the default position for when position is unspecified). For us here that means
The Discover tab 94
the .japxlate_app div which slots perfectly between the app header and footer. Setting a top and
bottom of zero here means our div will stretch to fill the area that .japxlate_app covers. (Which
conveniently gets rid of the padding-top:1em; we gave to .japxlate_app which is less than useful
here.) So this is going to remove the Twitter widget scrolling and width issues you say? Check
it out:
Figure 39. Widget scrolling removed but only for desktop Chrome
OK, so scrolling is fixed. But only on desktop Chrome and not the device. The widget width is
also still static:
Figure 40. Widget width is fixed and does not fill the available space
Well, we’ve got the perfect size container for this widget now, so what about forcing the width
and height of the generated iframe to the full size of this container? It’s actually pretty easy
to do this by adding some CSS rules for iframes in our index.css:
The Discover tab 95
iframe {
width:100%;
max-height:100%;
}
We simply say that the iframe should fill its parent’s width, and should never go taller than
the parent’s height. Run this on your device and it works! You can go landscape or portrait and
the widget always fills the available width and never scrolls.
We mentioned earlier some issues with iframes. Well, the Discover widget timeline contains
any links that the tweets themselves contain. Also, there are some buttons for Twitter “intents”
like replying, favouriting and retweeting. Clicking on any link actually opens that link in the
iframe - replacing the widget - which looks like this:
Figure 41. Links in the User timeline widget can be clicked - opening the page
Disaster! Clicking one of the Twitter intents (which would actually be kinda cool to get working
from the app), for example “reply”, flows like this: