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:
The Discover tab 96
Figure 42. Flow when clicking “reply” from timeline widget
So it already sorta kinda works in two different ways, but each has its issues. We are going to
let this slide for the first release of the app, but this issue really needs to be addressed. Perhaps
the widget itself has some useful customisation options? Or maybe links could be made to look
non-linky by some CSS in our app? Or JavaScript click catching?
9.2 Extra credit challenges
Solutions not provided. Try to add:
1. Disabling of clickable intents on tweets in the timeline widget OR..
2. ..Correct functioning of the clickable intents when the Twitter app is selected
(and not the browser)
3. Instead of simply displaying “(network connection required)”, use the Connec-
tion API plugin to properly detect if the device is currently online or not. If it’s
not online, then do something to inform the user that the Discover tab needs the
device to be online.
10. The Write tab
10.1 Layout and interface
Our third and final tab now which is Write. This is more complex than the previous Discover
tab, but slightly quicker to implement than the first Search tab - mostly due to not having any
scrolling woes to care about.
The write tab is going to be a little scratchpad area for the user to practice writing Japanese
phonic characters. We’ll present a random character and then an empty canvas for the user to
finger draw the character. A wireframe might look like this:
Figure 43. Quick wireframe of the Write tab layout
We simply present a character and a space for them to practice drawing it in.
Let’s start with the markup, and a dollop of CSS, first. Edit the write div of index.html to look
like this:
The Write tab 98
div id=write class=current
p style=text-align:center;Write this character:
span style=font-size:2em; id=char-to-write/span
span id=char-explanation/span
/p
canvas id=paper width=300 height=300/canvas
br
button type=button id=canvas-clear style=width:45%; margin-right:1%; float:ri
ght;
img src=img/paste.png Clear
/button
button type=button id=canvas-new style=width:45%; margin-left:1%;
img src=img/file.png New character
/button
/div
(Note that, like we did with the Discover tab, we’ve temporarily made this content div - and it’s
corresponding tab - class=current to speed up our development a tad.)
We have an explanatory paragraph for our random character, and placeholders for both the
character to write (in a larger font) and its explanation.
Then comes the magic and the main focus of this chapter, the whizz-bang HTML5 canvas ele-
ment which you may or may not have seen before. It’s an element that allows for programmatic
and interactive display and manipulation of simple 2D graphics. It’s kind of like Microsoft Paint
but in the browser and you have to program it with JavaScript. (It’s actually waaaaay better than
I’ve just made it sound!)
If you’ve got alarm bells ringing because we appear to have hard-coded the canvas dimensions
(300px x 300px) then you are right. We will come back and improve this shortly.
We end with two buttons, a Clear button which we want to erase the user’s scribbling so far,
and a New character button which will display a new random Japanese character to draw. Well
if you run all of this, it looks like:
The Write tab 99
Figure 44. Initial Write tab appearance
Clearly the canvas needs a bit of styling. Add a style for the canvas element to index.css
thus:
canvas {
border:1px solid grey;
background-color:#ffa; /*Post-It yellow*/
margin-left:auto; /*these two lines will*/
margin-right:auto; /*centre the element horizontally*/
display:block;
}
Running looks like this now:
Figure 45. Write tab appearance with styled canvas
The Write tab 100
Better!
10.2 Filling the screen
Hmmm, but Android devices come in all shapes and sizes. What to do about the size of the
canvas? We would want, ideally, the biggest square (Japanese characters tend to be rather
squareish) that would fit on the screen at that time - for portrait or lanscape.
We’ll take an approach like this:
1. Put the two buttons at the bottom of the screen
2. Make a containing div for the canvas that fills 100% of its available width and height
(the height under “Write this character” and above the buttons, then the full width of the
screen)
3. Set the size of the canvas to be the biggest square that will fit in this container. Centred in
the container
4. When the device is rotated, we will have to resize the canvas to be the biggest square in
the newly created container size
OK, tackling [1] and [2] first, we need a new layout. We are going to do the same thing we did
for the app’s header, content and footer; position them absolutely relative to parent. But this time
the parent will be our .japxlate_app div. Edit the write div in index.html to look like this:
div id=write class=current
p id=write-introWrite this character: span style=font-size:2em; id=char-to-
write/span
span id=char-explanation/span
/p
div id=write-canvas-container
!--canvas id=paper width=300 height=300/canvas--
/div
div id=write-buttons
button type=button id=canvas-clear style=width:45%; margin-right:1%; floa
t:right;
img src=img/paste.png Clear
/button
button type=button id=canvas-new style=width:45%; margin-left:1%;
img src=img/file.png New character
/button
/div
/div
We’ve given the intro paragraph an id. We’ve commmented the canvas out for the time being
but we’ve put it in a container div of #write-canvas-container. The buttons have also been placed
in a containing div of #write-buttons. Now we need to position and size these elements such that
the intro paragraph will be right at the top, the buttons will be right at the bottom, and the canvas
container will fill all the space inbetween. Add these CSS rules to index.css:
The Write tab 101
#write-intro {
position:absolute;
top:0; /*absolute top of .japxlate_app*/
height:40px; /*make arbitrarily big enough for our 2em character*/
width:100%;
margin-bottom:0; /*so #write-canvas-container is flush*/
margin-top:10px; /*so we aren't directly under the navigation tabs*/
text-align:center; /*centre text horizontally*/
/*background-color:green;*/
}
#write-canvas-container {
position:absolute;
top:50px; /*make flush with #write-intro*/
bottom:40px; /*stop 40px up from the botton of .japxlate_app*/
width:100%;
}
#write-buttons {
position:absolute;
bottom:0; /*absolute bottom of .japxlate_app*/
height:40px; /*make flush with bottom of #write-canvas-container*/
width:100%;
}
We simply make write-intro and write-buttons a little bit bigger than they need to be, and set
canvas container to fill the remaining space. We set margin-top of #write-intro to 10px so that
the intro paragraph text is not too close to the tabs and so that we know the top of #write-
canvas-container is 50px. We have previously set padding-top of .japxlate_app to 1em but this is
obliterated with the position:absolute and top:0 of #write-intro. (So yes, we’ve set a default
top padding of 1em on .japxlate_app but only used it in the Search tab as we positioned over it
on the Discover tab and this tab!). If you like you can confirm the size and shape of these divs
by setting a different background-color in each of the CSS rules and then changing the size of
desktop Chrome or rotating your device.
For [3] we need to programatically get the dimensions of #write-canvas-container, work out
the biggest square that will fit in those dimensions (possibly trimming a bit off so our canvas
isn’t too close to the buttons etc), and then dynamically add the appropriate canvas element into
#write-canvas-container - centreing it.
For [4] we need to catch a device rotation event and then do the steps for [3] again.
Create a file called canvas.js in assets/www/js. Include this JavaScipt file from the bottom of
index.html (above the include for index.js).
We are going to implement a function in canvas.js called adjustCanvas() which will be our
step [3]. adjustCanvas() looks like this:
The Write tab 102
//adjust - creating if necessary - the canvas element
function adjustCanvas()
{
var container = document.getElementById('write-canvas-container');
var style = window.getComputedStyle(container);
var width = parseInt(style.width);
var height = parseInt(style.height);
var smallestDim = width; //smallest dimension is width (= portrait)
if(height  width) //ie. landscape
{
smallestDim = height;
}
//invisible frame around canvas so it's not flush with buttons etc
var frameGap = 15;
smallestDim -= (frameGap * 2); //gap at top and bottom (left and right)
var canvas = null; //we proceed to get or create this
//element existence check
var firstTime = !document.getElementById('paper');
if(firstTime) //create canvas element with correct id
{
canvas = document.createElement('canvas');
canvas.id = 'paper';
canvas.style.position = 'relative'; //so we can top the canvas down
}
else //get existing canvas element
{
canvas = document.getElementById('paper');
}
//size and position the canvas
canvas.width = smallestDim;
canvas.height = smallestDim;
canvas.style.top = frameGap + 'px';
//add canvas (as child of container) if first time
if(firstTime)
{
container.appendChild(canvas);
}
}
The Write tab 103
Wow, this is our longest piece of JavaScript so far. With the first two lines we get the container
element for the canvas, and its style. We then save the width and height of the container.
We get the smallest dimension of the container (which will be the squared size of our canvas) by
assuming the container is portrait shaped. If container height is less than container width, we say
it is landscape shaped. (A perfectly square container will be covered by the portrait assumption
which will be fine as any of its side measurements is fine to use in that case.) We then put an
imaginary frame around the canvas of 15 pixels so that the bottom of the canvas is not flush
with our buttons.
Thinking ahead to step [4], it would be nice if this function to set up the canvas initially could
also be used to adjust the canvas for a device rotate. We tackle this with the concept of “first
time”. Basically, if the function is happening for the first time - ie. the canvas does not exist -
then it must create the canvas. If not the first time then it simply needs to alter the existing
canvas. We action this by:
var firstTime = !document.getElementById('paper');
Which relies on document.getElementById('someId') returning boolean false if an element
with the id of someId does not exist on the page. So we get or create the canvas ele-
ment and set its size to the smallest dimension of the container. Minus our frame gap of
course. The first time around we have to insert the created canvas element into the DOM
(document.createElement('elementname'); creates the element in memory only) which we do
with container.appendChild(canvas);.
Go ahead and delete the line:
canvas.style.position = 'relative';
and put that in the canvas{} rule in index.css as that makes more sense.
Next, mosey on over to firstLoadForTab_Write() in japxlate.js and add a call to
adjustCanvas() thus:
function firstLoadForTab_Write()
{
console.log('first load for write tab');
adjustCanvas(); //create canvas element of correct size
global_pagesLoaded.write = true;
}
Remember firstLoadForTab_Write() is our one-off initialiser for the Write tab and so running
the app now looks like:
The Write tab 104
Figure 46. canvas now fills available space
Pretty good! You can test that the canvas fills the available space by closing the page, resizing
the browser and loading the page again (desktop Chrome) or closing the app, rotating your phone,
reopening the app (device).
OK, that was actually the easy bit! Our next step is [4] and this is a bit fiddly as we need to figure
out how to detect a device rotation.
Well, the proper way is to catch the “orientationchange” event (of the window object) with a
handler. (Interestingly there is no PhoneGap API to do this.) Orientationchange will fire for
each and every orientation change of the device. And then in that handler you can use the
window.orientation property to work out the device orientation. Window.orientation will be 0
meaning portrait, 90 meaning landscape (top of phone pointing right) or -90 meaning landscape
(top of phone pointing left). These values represent the number of degrees the phone has been
rotated from the resting - or zero - position which is portrait. Android does not allow the screen
to be rotated upside-down and so there is no 180 value (phones only?).
As you would expect, our desktop Chrome browser doesn’t support the orientationchange event.
(You can try to spin your monitor around but don’t blame me if you wreck anything!). Keeping
our spirit of making the app work as an app and in the desktop Chrome, we are going to do
something a little different. (Although orientationchange is the “correct” way to do it.)
The closest thing to orientationchange on a desktop browser, and something that will also work
on our WebView browser, is the “resize” event of the window object. This event fires for every
resize - big or small - of the browser window. This includes the resize that our device will do to
the WebView when we rotate the device.
OK, let’s get the ball rolling (or should that be rotating? LOL). Stick a call to
configureCanvasRotationAdjustment(); at the bottom of receivedEvent() in index.js. We
define this function in canvas.js:
The Write tab 105
//device rotation handler
function configureCanvasRotationAdjustment()
{
window.addEventListener('resize', adjustCanvas, false);
}
Run this and you can see that the canvas resizes when you rotate the device OR when you
resize the desktop Chrome.
But there’s a problem, if you click on a different tab (not Write), rotate the device / resize the
browser and then click back on the Write tab, you get a tiny canvas like this:
Figure 47. canvas can become too small
What’s happening is this: Our onresize handler (adjustCanvas()) triggers when the browser
resizes, regardless of whichever tab we are on. The canvas container will not be visible when
not on the Write tab because of our whole tabbing mechanism. But adjustCanvas() will still get
called and create or adjust the canvas. It seems like .getComputedStyle() picks up the canvas
container smallest dimension as 100px when it is not visible. This resizes the canvas to a tiny
size - it just isn’t visible until you click the Write tab.
Remember we put adjustCanvas() in firstLoadForTab_Write()? This means that when we go
back to the Write tab after clicking another tab, adjustCanvas() is not called. One solution
would be to somehow only action the resize handler when on the Write tab. But what we
will do instead is move the call to adjustCanvas() out of firstLoadForTab_Write() and into
onclickForTab_Write() such that the canvas is adjusted on every clicking of the Write tab. So re-
move adjustCanvas() from firstLoadForTab_Write() and stick it in onclickForTab_Write()
so it looks like this:
The Write tab 106
function onclickForTab_Write()
{
console.log('click on write tab');
if(!global_pagesLoaded.write)
{
firstLoadForTab_Write();
}
adjustCanvas(); //adjust - creating if necessary - the canvas element
}
Run this and the problem has been resolved. Nice.
Phew, so the canvas display and layout is pretty hot and tasty right now and should be
appropriate for any device that the app runs on.
10.3 Displaying a random character
Now, even before we get to the New character and Clear buttons, we need to present a random
Japanese character for the user to draw. And of course we need to make finger movements on
the canvas actually write something!
Put a call to the soon-to-be-implemented doNewChar() at the end of onclickForTab_Write() (so
that every click on the Write tab will present a new character to practice) such that it looks like
this:
function onclickForTab_Write()
{
console.log('click on write tab');
if(!global_pagesLoaded.write)
{
firstLoadForTab_Write();
}
adjustCanvas(); //adjust - creating if necessary - the canvas element
//get and display a random Japanese character to practice writing
doNewChar();
}
doNewChar() is going to get a random Japanese character, and display it above the canvas for the
user’s reference. So before we implement doNewChar(), we need a slight detour - we need the
function to get a random Japanese character! We will put this in, you guessed, linguistics.js:
The Write tab 107
//return a random (non-chiisai, non-obsolete) hiragana or katakana
//RETURNs object like {char:'', romaji:'ku', type:'hiragana'}
function getRandomKana()
{
//indices to ignore from coreHiragana:
//64,65,68,=74
//so this is indices of coreHiragana of chars that we WANT to practice
//(ie. not chiisai or obsolete):
var coreIndices =
[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 66, 67, 69, 70, 71, 72,
73
];
//get one of above indices at random
var index = coreIndices[Math.floor(Math.random() * coreIndices.length)];
//default to hiragana...
var char = coreHiragana[index]; //use our random index
var type = 'hiragana';
//...but have a 50% chance of returning katakana
if(Math.random()  0.50)
{
char = hira_to_kata(char);
type = 'katakana';
}
//return a useful object
return {char:char, romaji:kana_to_romaji(char), type:type};
}
This again might be something that’s best ignored and treated as a black box, but basically we
cherry pick a phonetic character from coreHiragana based on some desired indices (to give us a
full size and non-obsolete character). We then convert that character into katakana 50% of the
time. We return an object with the character itself and some metadata.
Let’s use this function in doNewChar() which we define in canvas.js:
The Write tab 108
//Get and display (with explanation) a random Japanese character
//to practice writing
function doNewChar()
{
var randomKana = getRandomKana();
document.getElementById('char-to-write').innerHTML = randomKana.char;
document.getElementById('char-explanation').innerHTML = '(' + randomKana.romaji + 
' in ' + randomKana.type + ')';
}
Running the app now looks like this:
Figure 48. Random Japanese character with metadata is displayed
Pretty cool! We get the random character to practice writing in a nice big font, the English spelling
/ pronunciation and the type of script it’s from.
Wait, I’ve just had a good idea. How about, before we let them start writing, if we flash up the
character on the canvas, filling the canvas and then fading out for them to start copying it! That’s
gonna be awesome!
Note that this next bit (an initial, fading character on the canvas) is going to be somewhat
difficult and involves recursive JavaScript.
We’ll want to do this character fading for every new character that we present. So a good place
to call our fading function will be from doNewChar() in canvas.js. Add this as the last line in
doNewChar():
The Write tab 109
//put character on canvas and fade to nothing (starting at 1 opacity)
//our trademark #990000 red is rgb(153, 0, 0)
fadeCharOnCanvas(randomKana.char, 153, 0, 0, 1, 1, 100, 10);
We call fadeCharOnCanvas() with a lot of parameters. Let’s implement fadeCharOnCanvas() -
also in canvas.js - right now. It’s a biggie because it uses a few helper functions and variables
that we’ll implement shortly. So don’t be scared if you see something that hasn’t been referenced
yet! OK, implement fadeCharOnCanvas() to look like this:
//Animate a fade out of a single (Japanese) character on the canvas.
//Starting from opacity of startAlpha and stepping down to zero opacity
function fadeCharOnCanvas(char, startR, startG, startB, startAlpha, thisAlpha, msDelay,
frameCount)
{
//calc the step down amount
var dec = startAlpha / frameCount;
//what will the *next* opacity be?
var nextAlpha = thisAlpha - dec;
//console.log('thisAlpha:' + thisAlpha + ' -- nextAlpha:' + nextAlpha);
//dues to floating point rounding we prob won't reach exactly zero
//BUT we want exactly zero for the char to disappear
//SO if thisAlpha is on the last frame, force to zero
if(thisAlpha = (startAlpha - ((frameCount - 1) * dec)))
{
//console.log('last frame reached');
thisAlpha = 0;
}
clearCanvas(); //else we are drawing a lighter char over a darker one!
//console.log(global_canvasElement.width);
var fontSize = parseInt(global_canvasElement.width * 0.833);
//console.log(fontSize);
global_canvas.font = 'normal ' + fontSize + 'px serif';
global_canvas.fillStyle = 'rgba(' + startR + ',' + startG + ',' + startB + ',' + th
isAlpha + ')'; //rgb alpha
global_canvas.fillText(char, parseInt(global_canvasElement.width * 0.05), parseInt(
global_canvasElement.width * 0.78));
if(nextAlpha  0)
{
//console.log('last frame drawn - exiting');
return;
}
The Write tab 110
//recurse!
setTimeout(function(){fadeCharOnCanvas(char, startR, startG, startB, startAlpha, ne
xtAlpha, msDelay, frameCount)}, msDelay);
}
The canvas API lets us write text in any colour and at any opacity. We set the opacity using
what’s called an alpha value; one meaning fully opaque and zero meaning fully transparent.
In a nutshell, we simply draw the character on the canvas a bunch of times and increase the
transparency at each step. OK, let’s have a fuller explanation…
We accept, in order of appearance, these parameters:
1. char - the single Japanese character to draw
2. startR - red element of colour to use for drawing (integer 0 - 255)
3. startG - green element of colour to use for drawing (integer 0 - 255)
4. startB - blue element of colour to use for drawing (integer 0 - 255)
5. startAlpha - opacity used to draw the first frame (float 0.0 - 1.0)
6. thisAlpha - opacity used to draw the current frame (float 0.0 - 1.0)
7. msDelay - the delay, in milliseconds, between each frame
8. frameCount - how many frames to take to get down to zero opacity
(Note that startR, startG and startB don’t actually change value at any step of the recursion.)
We save in dec the amount we have to decrement the starting opacity (startAlpha) by each step
to reach zero opacity after the specified number of frames (frameCount).
We then use dec to work out what the opacity of the next frame will be. We’ll use this value a
bit later on.
Next we have a workaround for the vagaries of floating point arithmetic. Basically, due to
rounding, we might not reach an opacity of exactly zero on our last frame. So we do an if
check here to see if we are more-or-less on the last frame now (ie. (frameCount - 1) frames have
already happened). If we are, we force the current opacity to zero.
Next, and before we draw anything, we clear the canvas. We will define clearCanvas() shortly.
Next we calculate fontSize to make the character best fill the canvas. During development I
saw that for a 300px by 300px canvas, a font size of 250px was best. This is the equivalent of the
font size being 83.3% of the canvas size. Trial-and-error with some different canvas sizes told me
that this percentage just works. global_canvasElement is the canvas DOM element which we
need to have available when this function is called.
Next we use the font property of global_canvas to set the font size we just calculated. global_-
canvas is the drawing context of our canvas element which we need to have available when
this function is called. We cover this shortly so don’t worry.
Next we use an rgba (“red, green, blue, alpha”) value to set the global_canvas.fillStyle
property. This sets the colour and opacity that our character will get drawn with.
Then we call global_canvas.fillText() which actually writes our character on the canvas. The
second argument is the canvas x coordinate where you want the (left edge of the) text to start
The Write tab 111
drawing. The third argument is the canvas y coordinate where you want the (bottom baseline
of the) text to be. Again by trial-and-error I saw it was best for the x coordinate to be 5% of the
canvas total width, and the y coordinate to be 78% of the canvas total height.
OK, that’s one frame drawn, but we are still in the function and so we need to exit the function
if we have just drawn the last frame. That’s what we do with the next if check.
Last but not least, if we are still in the function then we have another frame to draw. We
make the function call itself (recursion) at the end. But this should only happen after the
specified delay (msDelay) and so we use JavaScript’s builtin setTimeout() method (of the window
object) to call ourself after the delay. setTimeout() works like setTimeout(someCallableThing,
delayInMilliseconds);. We simply call ourself with the updated value for nextAlpha - all the
other arguments are the same.
Great! But we still need to define clearCanvas() and initialise global_canvas and global_-
canvasElement. Let’s do the globals first, define them at the top of canvas.js like this:
//The drawing context of our canvas
var global_canvasElement;
//Our canvas DOM element
var global_canvas;
Nothing difficult there. Now, again in canvas.js, define initialiseCanvas() thus:
//Save our global canvas variables
function initialiseCanvas()
{
//set the globals
global_canvasElement = document.getElementById('paper');
global_canvas = global_canvasElement.getContext('2d');
}
Good old document.getElementById() is used to save the canvas DOM element into global_-
canvasElement. For global_canvas we call getContext('2d') on our canvas DOM element.
This is a builtin method of the HTML5 canvas object and will return an API for 2D drawing.
(Yes, there’s a 3D API - getContext('webgl') - but it won’t work in as many browsers as the
2D API.)
The question now is where to call initialiseCanvas()? If you’re thinking that we need to call
it once and only once, then you’re thinking the same as me. However, during development I
noticed an obscure quirk with the canvas drawing context whenever the canvas was resized (ie.
we rotate our device): the canvas drawing context would reset! With this in mind we need to call
initialiseCanvas() every time the canvas is adjusted. This is actually easy to do and simply
involves adding:
initialiseCanvas();
as the very last line of adjustCanvas().
Just clearCanvas() left now. Stick it in canvas.js and it goes like:
The Write tab 112
//Clear the canvas
function clearCanvas()
{
global_canvas.clearRect(0, 0, global_canvasElement.width, global_canvasElement.heig
ht);
}
We use the clearRect() canvas API method to clear a rectangle on the canvas. The rectangle we
clear starts at (0, 0) in canvas space and the width and height matches that of the canvas itself.
This clears the entire canvas.
All done! You should now get the Japanese character fading down on the canvas for each new
character!
10.4 Finger doodling
Right, on to the biggie now which is making something draw on the canvas when the user
moves their finger in there. Do you remember our work on finger scrolling for the Search tab?
Remember we got it working on the device first then we simulated finger presses - using mouse
events - for debugging in desktop Chrome? Well, let’s do it the other way round this time. Let’s
get canvas writing working on desktop Chrome first.
When the mouse is down in the canvas, and then the mouse is moved, we want to leave a “trail”
on the canvas as if drawing with a calligraphy brush or what-have-you. I mentioned earlier that
HTML5 canvas is somewhat like MS Paint in the browser. Like MS Paint, it has a line drawing
API where we can start the “brush” at a certain coordinate, and then paint a line from there to
any other coordinate.
And, like we did for simulated finger scrolling, we are going to need a global variable to track
the mouse downness. In fact, we’ll recycle the same global variable (so yes, it might not logically
belong in a file called search_interface.js).
You remember our initialiseCanvas() in canvas.js which is where we stored the canvas in
global variables just after creating it for the first time. That seems like a good place to put the
event listeners we’re going to need for our mouse drawing. Add three addEventListener() calls
- on the canvas element - at the bottom of initialiseCanvas() such that it now looks like this:
function initialiseCanvas()
{
//set the globals
global_canvasElement = document.getElementById('paper');
global_canvas = global_canvasElement.getContext('2d');
//simulated touch (ie. mouse) dragging for writing pad
global_canvasElement.addEventListener('mousedown', mousedownForCanvas, false);
global_canvasElement.addEventListener('mousemove', mousemoveForCanvas, false);
global_canvasElement.addEventListener('mouseup', mouseupForCanvas, false);
}
The Write tab 113
You’ve guessed it, we’re going to define mousedownForCanvas(), mousemoveForCanvas() and
mouseupForCanvas() right now; also in canvas.js:
//Mousedown event handler for canvas - set now drawing state
function mousedownForCanvas(event)
{
global_mouseButtonDown = true;
event.preventDefault();
}
//Mousemove event handler for canvas - draw on canvas
function mousemoveForCanvas(event)
{
var x, y;
// Get the mouse position relative to the canvas element.
//(ie. the mouse position IN CANVAS SPACE)
if (event.offsetX || event.offsetX == 0) { // Opera and Chrome
x = event.offsetX;
y = event.offsetY;
}
//This event handler works like a drawing pencil which tracks the mouse
//movements. We start drawing a path made up of lines.
if(global_mouseButtonDown)
{
doDrawOnCanvas(x, y); //our canvas API magic
}
}
//Mouseup event handler for canvas - unset now drawing state
function mouseupForCanvas(event)
{
//console.log('mouseup event on canvas');
global_mouseButtonDown = false;
global_startedDrawing = false;
event.preventDefault();
}
mousedownForCanvas() simply sets our recycled global_mouseButtonDown to true. In
mousemoveForCanvas() we receive the mouse event and, if the mouse button is down, call
doDrawOnCanvas() with canvas space x and y coordinates. Specifically we pass it offsetX and
offsetY of the mouse event. In Chrome (and also Opera but definitely not Firefox), this is the xy
coords of the mouse event but offset to be in element space. Element space meaning the very
top-left of the element is x=0, y=0 and so on.
Why call doDrawOnCanvas() and not just do the canvas drawing logic in the mousemove
handler? Well it’s because, like we did for search results scrolling, we want to reuse the same
The Write tab 114
logic for actual finger drawing and simulated finger drawing with the mouse. We’ll get on to
doDrawOnCanvas() in a moment.
Finally in mouseupForCanvas() we set global_mouseButtonDown to false. We also set global_-
startedDrawing to false but we haven’t defined that yet. Let’s define it first then I’ll explain.
Stick:
//Are we already drawing on the canvas?
var global_startedDrawing = false;
at the top of canvas.js with the other globals.
We need to keep track of this because, as we will see shortly, with canvas API we have two
very distinct steps of drawing lines; moving the brush to a certain point to start the path
and then actually moving the brush from that point. So most of the magic happens in our
doDrawOnCanvas() which we will define right now, also in canvas.js:
//Universal canvas line plotter. for onmousemove etc
function doDrawOnCanvas(canvasX, canvasY)
{
//This works like a drawing pencil which tracks the mouse / touch
//movements. We draw a path made up of lines.
if(!global_startedDrawing) //first time
{
global_canvas.beginPath();
global_canvas.moveTo(canvasX, canvasY);
global_startedDrawing = true;
}
else
{
global_canvas.lineTo(canvasX, canvasY);
global_canvas.stroke();
}
}
We expect to receive x and y coordinates in canvas space, which is conveniently provided by
.offsetX and Y on the mousemove event. (The offsetX and Y properties will contain the screen
coordinates of the mouse converted into element space such that if the mouse is in the very
top-left of the element - our canvas - the offsetX and Y will be 0,0.)
For the first time that drawing has started, we call a couple of canvas API methods to get ready for
drawing. We call beginPath() which starts a new path. A path is basically a line or curve that we
draw onto the canvas. We then call moveTo() which positions the “brush” but does not actually
paint anything. We then, for subsequent mousemoves or finger drags set global_startedDrawing
to true so that the next bit can happen…
Which is simply to call two more canvas API methods. We call lineTo() with the updated mouse
/ finger position (again in canvas space) which will move the brush from its path starting point
The Write tab 115
to the point specified. Note that even this will not paint anything on the canvas. For that we need
to call stroke() which will actually follow the path and put the “ink” down.
Give it a whirl in desktop Chrome! You should be able to paint on the canvas using the mouse!
Figure 49. Canvas painting with mouse
Hmmm, but that thin black line doesn’t feel so great. Let’s fatten it out and make it our
signature red. HTML5 canvas exposes a bunch of properties that we can tinker with to affect line
drawing style. The right place to change these settings feels like our one-off canvas initialiser of
initialiseCanvas(). Add two lines to the bottom so it looks like:
function initialiseCanvas()
{
//set the globals
global_canvasElement = document.getElementById('paper');
global_canvas = global_canvasElement.getContext('2d');
//simulated touch (ie. mouse) dragging for writing pad
global_canvasElement.addEventListener('mousedown', mousedownForCanvas, false);
global_canvasElement.addEventListener('mousemove', mousemoveForCanvas, false);
global_canvasElement.addEventListener('mouseup', mouseupForCanvas, false);
//line drawing style
global_canvas.strokeStyle = '#990000'; //our trademark red
global_canvas.lineWidth = 10;
}
We can set the colour that stroke() will use with .strokeStyle. We can set the line width with,
yes, .lineWidth.
Try it!:
The Write tab 116
Figure 50. Line angles and edges somewhat jaggedy
Much better! The red looks great and it feels nice and fat like a calligraphy brush. But, and this is
much easier to notice when you’re actually drawing with it and not looking at this screenshot,
there’s something not quite right. The drawn line seems to have some quite sharp edges and
generally looks jaggedy.
Digging deeper into the canvas API, we have some interesting properties to change the brush
drawing style. The lineJoin property of the canvas
(http://www.html5canvastutorials.com/tutorials/html5-canvas-line-joins is a useful page) spec-
ifies how we want to draw the “join” between two lines which in our case basically means the
point where we change the direction of drawing with our brush. The default is “miter” which is
a very sharp join. You can see this on the above figure. The remaining options are “bevel” and
“round”. We’ll go for round as this most resembles what a calligraphy brush would do.
There is also the “lineCap” property (http://www.html5canvastutorials.com/tutorials/html5-
canvas-line-caps is a useful page) which is how to draw the end of the line. As you can see from
the above figure, the default is “square”. For that calligraphy feel, let’s make this also “round”.
So add to the end of initialiseCanvas() so it looks like this:
function initialiseCanvas()
{
//set the globals
global_canvasElement = document.getElementById('paper');
global_canvas = global_canvasElement.getContext('2d');
//simulated touch (ie. mouse) dragging for writing pad
global_canvasElement.addEventListener('mousedown', mousedownForCanvas, false);
global_canvasElement.addEventListener('mousemove', mousemoveForCanvas, false);
global_canvasElement.addEventListener('mouseup', mouseupForCanvas, false);
//line drawing style
global_canvas.strokeStyle = '#990000'; //our trademark red
global_canvas.lineWidth = 10;
global_canvas.lineCap = 'round'; //dat calligraphy feel
global_canvas.lineJoin = 'round'; //dat calligraphy feel
}
Try running the app now and it draws like this:
The Write tab 117
Figure 51. Line angles and ends have a rounder feel
Awesome! I feel warm and fuzzy inside.
Right, let’s quickly get our bottom buttons in the bag and then we’ll get our calligraphy brush
working with our sticky fingers on our actual device. Okey dokey, we’re going to need some
event handlers to do the appropriate things for our New character and Clear buttons. Stroll over
to index.js and whack a call to configureCanvasButtons(); at the end of receivedEvent().
receivedEvent() will look like this now:
// 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();
configureSearchButton();
configureSearchInput();
configureSearchTouchScrolling();
configureSearchMouseScrolling();
configureCanvasRotationAdjustment();
configureCanvasButtons();
},
We’ll define configureCanvasButtons() in canvas.js thus:
The Write tab 118
//New character and Clear buttons
function configureCanvasButtons()
{
document.getElementById('canvas-clear').addEventListener('click', clearCanvas, fals
e);
document.getElementById('canvas-new').addEventListener('click', doNewChar, false);
}
And that’s it! The Clear button simply calls our existing clearCanvas() function, and the New
character button simply calls our existing doNewChar() function. Try it! The write tab is much
more fun now!
Right, the remaining matter is the important one of getting drawing to work with finger moves
on the device itself. We are happy with how it works in desktop Chrome now so let’s move
forward and think about the device. We isolated the doDrawOnCanvas() function and it is ready
to accept canvas coordinates from any event, not just mouse events.
Just like with search results scrolling, the events in question are touchstart, touchmove and
touchend. These will somewhat correlate with the mousedown, mousemove and mouseup
handlers (respectively) that we’ve just implemented.
In initialiseCanvas() in canvas.js, go ahead and insert these three lines to add our touch
event handlers. Insert them after the mouse event handlers:
//touch dragging for writing pad
global_canvasElement.addEventListener('touchstart', touchstartForCanvas, false);
global_canvasElement.addEventListener('touchmove', touchmoveForCanvas, false);
global_canvasElement.addEventListener('touchend', touchendForCanvas, false);
Great, now let’s define these handlers; again in canvas.js:
//Touchstart event handler for canvas
function touchstartForCanvas(event)
{
//console.log('touchstart event on canvas');
event.preventDefault();
}
//Touchmove event handler for canvas - draw on canvas
function touchmoveForCanvas(event)
{
var touchobj = event.changedTouches[0]; //reference first touch point for this event
//where, in canvas space, has been touched?
var x, y;
x = touchobj.offsetX;
y = touchobj.offsetY;
The Write tab 119
doDrawOnCanvas(x, y);
}
//Touchend event handler for canvas - unset now drawing state
function touchendForCanvas(event)
{
//console.log('touchend event on canvas');
global_startedDrawing = false;
event.preventDefault();
}
We don’t actually do anything in the touchstart handler, but we’ll keep it in case we do need to
do anything in future.
In the touchmove handler we get the first touch point and, like we did for mousemove, pass its
offsetX and Y to doDrawOnCanvas().
Finally the touchend handler simply sets the global “started drawing” state to false; ready for the
next scribble.
OK, so run this on your device and, hmmmm, finger drawing does not work! What gives? The
problem is that the touch object we get from the touchmove event does not have offsetX or Y
properties! Nor does the touchmove event itself! This is officially A Very Annoying Thing. I
think the JavaScript implementors are missing a bit of a trick here honestly. Without offsetting
the touch object coordinates, they will be screen coordinates and will not correlate to canvas
space. They will basically be too big to point to anywhere meaningful in the canvas.
The good news is that every DOM element under body exposes some offset properties.
Specifically we have .offsetParent which points to an element’s parent element, and we have
.offsetLeft and .offsetTop which tell us, in pixels, how far away from the top-left of the parent
the top-left of the element is.
The bad news is that most elements, in particular our canvas, have multiple parents. We are
going to have to programatically loop up through an element’s parents and tot up the offsets
to work out a true screen offset of an element. It’s actually a trivial function and let’s put it in
canvas.js:
//Get DOM element position on page
function getPosition(obj)
{
var x = 0, y = 0;
if (obj.offsetParent)
{
do
{
x += obj.offsetLeft;
The Write tab 120
y += obj.offsetTop;
obj = obj.offsetParent;
}
while(obj);
}
return {'x':x, 'y':y};
};
We accept a DOM element in obj. We start the x and y values - our offset - at zero. Then, if we
have an .offsetparent on obj (if not then it is body and will have no parent) we loop (at least
once) cumulatively adding .offsetLeft to x and .offsetTop to y. We loop as long as there is a parent
up the chain. We return the coordinates in a little object.
Right, with this we can go back and fix touchmoveForCanvas(). Edit touchmoveForCanvas() to
look like this:
function touchmoveForCanvas(event)
{
var touchobj = event.changedTouches[0]; //reference first touch point for this event
//where, in canvas space, has been touched?
var x, y;
var canvasOffset = getPosition(global_canvasElement); //why no offset x and y for
if it's a touch event? :-(
x = touchobj.screenX - canvasOffset.x;
y = touchobj.screenY - canvasOffset.y;
doDrawOnCanvas(x, y);
}
Run this on your device. It works!
OMG, we’ve nailed it, we’ve finished all the tabs! Crack open the beers at this point ;-)
Just two more easy things. Let’s have a splashscreen that displays while we are waiting for the
app to start. Also, we’re going to need something other than PhoneGap’s default launcher icon!
10.5 Extra credit challenges
Solutions not provided.
Easy
Prevent the current character from being displayed again when clicking the New
character button
The Write tab 121
Medium
The way we have centred the big fading character on the canvas is slightly ridiculous.
Use the canvas API to properly centre the character on the canvas [NOTETOSELF
find the good link for this]
Difficult
Technically what we are doing here for finger doodling is following the mouse or finger and,
underneath it, creating a multi-sectional line. We aren’t actually simply placing a pixel under
the mouse or finger. Now, the effect is the same so it mostly doesn’t matter, but there is a side
effect of doing it this way which is that, on the device, if you draw a very quick semi-circle, for
example, it will end up looking like this:
Figure 52. Imperfect lines can be created
Re-engineer finger doodling to use HTML5 canvas’s pixel manipulation API and see
if that solves this problem. http://beej.us/blog/data/html5s-canvas-2-pixel is a useful
reference here (though a few years old now)
11. Splash screen
To kill the dragon, turn to page 84. To hide in the tunnel, keep reading. LOL that was an interactive
fiction reference, because this entire chapter is optional. Why? Well it’s about implementing a
splash screen which is a contentious issue in the world of Android apps. (It also involves fiddling
with our app’s Java sources which is a bit advanced for this book.)
A splash screen is displayed after tapping an app’s launcher icon. They tend to fill the screen and
disappear after a delay and / or when the app is ready. Plenty of apps don’t have one. Most big
name games have one as those apps can take a while to load. Small utility apps (which is what
Japxlate is) seem to mostly not have one.
Why is this contentious? Well, the thinking is that they form a barrier to using the app. But they
are something to look at when larger apps are loading which could be useful.
So make up your own mind (http://cyrilmottier.com/2012/05/03/splash-screens-are-evil-dont-
use-them is useful) and skip this chapter if you feel Japxlate doesn’t need a splash screen. If
you think it does need one - or just want to see how it generally works - then please read on.
Hopefully only the first time while the Web SQL database is created, but our app may take a while
to start, and we also want to have some branding. So, we will have a splash screen. PhoneGap
exposes splash screen control in the “Splashscreen” API. This needs to be installed as a plugin
which can be done like this:
you@yours$ japxlate]$ phonegap local plugin add https://git-wip-us.apache.org/repos/asf
/cordova-plugin-splashscreen.git
From PhoneGap v3.3.0 you can simply type phonegap local plugin add
org.apache.cordova.splashscreen
Under the hood, this command will do a few things for you:
• Add feature name=SplashScreen to res/xml/config.xml
• Put SplashScreen.java in newly created /src/org/apache/cordova/splashscreen folder
(so this is an example of a plugin that will add to the native Java sources)
• Put the plugin in /assets/www/plugins/
• Add references to the plugin in /assets/www/cordova_plugins.js
Because of the new Plugman, PhoneGap v3.3.0 instead puts the plugin in
PROJECTROOT/plugins
Splash screen 123
So now we’ve got the JavaScript API exposing splash screen actions (show or hide). But this plu-
gin has also added some native Java (the “SplashScreen” class). Hold on to your hats folks because
we’re going to have to fiddle with the app’s Java source code! Don’t worry though, it’s pretty
straightforward. Open up Japxlate.java (which is in /src/com/drappenheimer/japxlate), it
will look like this:
.
.
public class Japxlate extends CordovaActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
super.init();
// Set by lt;content src=index.html / in config.xml
super.loadUrl(Config.getStartUrl());
//super.loadUrl(file:///android_asset/www/index.html)
}
}
This is the constructor code that gets the app up and running when the launcher icon is tapped.
All we do here is load (indirectly from config.xml) index.html into the current activity - which
is a WebView browser. Change it to this:
public class Japxlate extends CordovaActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
super.init();
// Set by lt;content src=index.html / in config.xml
super.setIntegerProperty(splashscreen, R.drawable.splash);
super.loadUrl(Config.getStartUrl(), 5000); //5 seconds timeout for splash
//super.loadUrl(file:///android_asset/www/index.html)
}
}
Before super.loadUrl(), we use super.setIntegerProperty() to set the “splashscreen” prop-
erty to R.drawable.splash. “R” is a piece of Java and XML magic which basically picks up all the
files and resources in the “res” folder and exposes them as useable properties of the “R” object.
R.drawable.splash refers to the appropriate splash.png placed in the /res/drawable folder.
splash.png will be our image file for the splash screen. We are about to make that, but first let’s
take a peek at the /res folder structure:
Splash screen 124
Figure 53. Contents of /res folder (Eclipse IDE)
Opening up these folders we see:
Figure 54. Exploded contents of /res folder (NetBeans IDE)
The icon.png files are the PhoneGap default launcher icons (we’ll be changing these a bit later).
As the name suggests, the drawable folder is where we place any kind of image resource that
our app needs. What’s going on here is that, as well as the generic drawable folder, we have four
other drawable folders tailored to different screen pixel densities. A high-end Android device
will have well over 300 pixels-per-inch. In fact a full-HD phone with a compactish (5” or less)
display will probably have over 400! Imagine our launcher icon is fixed at 32 pixels square. This
is going to be tiny on a display with 400 pixels-per-inch! The more dense the display pixels are,
the larger our launcher icon needs to be to stay the same physical size and to show the same
amount of detail. This is true for splash screen images, launcher icons, and any drawable. Back
to these folders, we have:
• drawable-ldpi - “low dots-per-inch” (default launcher icon = 36x36)
Splash screen 125
• drawble-mdpi - “medium dots-per-inch” (default launcher icon = 48x48)
• drawable-hdpi - “high dots-per-inch” (default launcher icon = 72x72)
• drawable-xhdpi - “eXtra high dots-per-inch” (default launcher icon = 96x96)
(Gory details at http://developer.android.com/guide/practices/screens_support.html)
So for our splash screen we need to put a splash.png of correct size in each of these folders.
But just to make sure our code works first we can simply put one splash.png in the “drawable”
folder. Our device will suss that there is no specific splash image for its native DPI and simply
display the default one in the “drawable” folder. Which obviously might look hideous if the image
is too small for the display (or vice versa). Let’s do that first just to get the code working and
then we can put in the proper images.
OK, so make (or get from the Japxlate sources) a muckabout png in portrait shape of size 320x470.
It’s going to be the Japxlate “J” in our signature red.
Put the png file in the drawable folder and run the app on your device. You should get the image
splashed onto the display for 5 seconds before the app starts. It might look very stretched out and
jaggedy. Note that the splash screen will not appear when running the app in desktop Chrome
(which is a good thing).
..
If you’ve added the status bar to the app as discussed at the end of the First things first: The
layout chapter, you’ll still have the status bar when the splash screen is being displayed.
Hmmm, but what if 5 seconds is too long? The app might load much quicker than that and we
want the splash screen to disappear as soon as the app loads. Well I remember seeing something
in config.xml:
preference name=auto-hide-splash-screen value=true /
So maybe the splash screen is auto hiding as soon as the app is ready, and, by coincidence, our
app is taking 5 seconds to load? Well, let’s test that by changing the timeout in Japxlate.java
to 20 seconds:
.
.
public class Japxlate extends CordovaActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
super.init();
// Set by lt;content src=index.html / in config.xml
super.setIntegerProperty(splashscreen, R.drawable.splash);
Splash screen 126
super.loadUrl(Config.getStartUrl(), 20000);
//super.loadUrl(file:///android_asset/www/index.html)
}
}
Run this and we see that the splash screen does indeed hang around for 20 seconds. The
app is not quite that slow and so we need a way to hide the splash screen when the
app is ready. We learn from http://docs.phonegap.com/en/3.1.0/cordova_splashscreen_splash-
screen.md.html#Splashscreen that:
“To dismiss the splash screen once the app receives the deviceready event, call the
navigator.splashscreen.hide() method.”
Let’s try that. Edit the receivedEvent() method in index.js to look like this:
// Update DOM on a Received Event
receivedEvent: function(id) {
console.log('Received Event: ' + id);
if (window.cordova) { //actual app
navigator.splashscreen.hide();
}
.
.
As the whole of receivedEvent() is called from both our actual device and debugging in
desktop Chrome*, we need to check for window.cordova - ie. we’re on the device - before calling
navigator.splashscreen.hide();.
*Which does not need to be the case but is just how our simple app has evolved.
Run this on your device and you will see that the splash screen disappears when the app is ready,
much sooner than 20 seconds! Great!
OK, we now need to create the actual splash screen images in the correct sizes. The correct sizes
for each image should be:
• xlarge (xhdpi): at least 960 × 720
• large (hdpi): at least 640 × 480
• medium (mdpi): at least 470 × 320
• small (ldpi): at least 426 × 320
So go ahead and create (or get from the Japxlate repo) your final “J” images in these sizes and
put them in the correct drawables folders.
Splash screen 127
..
If the image with the correct density for the device is missing, the app seems to find and use
the closest matching one.
Just one problem remains. Rotate your device into landscape and launch the app. Yuck! Did you
see the splash image? It was really stretched out and fat.
Figure 55. Splash screen when in landscape orientation
Well here’s the correct way to solve this. Really for splash images - and other drawables - you
should use a “9 patch” graphic. This is a tricky little format where PNG images have a one-pixel
border that isn’t displayed, but contains control pixels telling Android which bits of the image to
stretch, shrink or leave unaltered when the device is rotated or if the target device’s aspect ratio
is not the same as the drawable file in the app.
As you can see already, normal PNGs placed in the drawable folder essentially work, so this
tutorial won’t cover 9 patch images any more other than to say that the gory details are here:
https://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch
“A NinePatchDrawable graphic is a stretchable bitmap image, which Android will
automatically resize to accommodate the contents of the View in which you have
placed it as the background. An example use of a NinePatch is the backgrounds
used by standard Android buttons — buttons must stretch to accommodate strings
of various lengths. A NinePatch drawable is a standard PNG image that includes an
extra 1-pixel-wide border. It must be saved with the extension .9.png, and saved into
the res/drawable/ directory of your project.”
and that the Android SDK has a little tool called draw9patch (in ANDROID_SDK_HOME/sdk/tools)
to make these images which looks like this:
Splash screen 128
Figure 56. Android SDK’s draw9patch utility
The PhoneGap / Cordova documentation for v3.3.0 now has a useful summary section
on Icons and Splash Screens.
12. Launcher icon
Now we move on to our launcher icon. We are provided with a default one which is like a robot
cube thing. Let’s replace this with one more relevant for Japxlate. Let’s have one with our “J”.
We simply replace the icon.png files in the various drawable folders. The size of each existing
icon.png will tell you the required icon size for that density.
http://developer.android.com/design/style/iconography.html is a useful design and technical
reference.
Note that you might, in order for the icon to update on deploy to the device, need to do a Project
⇒ Clean in Eclipse IDE.
The PhoneGap / Cordova documentation for v3.3.0 now has a useful summary section
on Icons and Splash Screens.
13. Submitting to Google Play
[NOTETOSELF expand on this section]
This topic deserves - and has - whole books and tutorials dedicated to it. [NOTETOSELF some
links would be nice] We’ll cover it in a whistle-stop fashion. To publish your app on the Play
Store, follow these steps:
1. Open PROJECTROOT/AndroidManifest.xml and change android:debuggable=true to
android:debuggable=false
2. In Eclipse IDE, right-click on the Project and select Android Tools ⇒ Export Signed
Application Package
3. Follow the steps (choosing “create new keystore”) and be sure to save the keystore file and
password, and the alias password, in your favourite secure place for future reference
4. The final step creates an .apk file in the location you specify. This is what we will upload
to google.
5. You will need to attach an Android Developer account to an already existing vanilla Google
account. Do this by logging into Google with your vanilla account first, then signing up
as an Android developer at https://play.google.com/apps/publish/signup.
6. You’ll need a credit or debit card handy as there is a one-off registration fee of $25.
7. Upload the .apk file to your shiny new Developer account, set the title, description and
screenshots (you also need a 512px x 512px hi-res icon) and Bob’s your uncle!
14. That’s all folks!
[NOTETOSELF this is going to be a wrapping up chat. talk about the app’s pros and cons,
PhoneGap pros and cons. And talk about what they need to do and research going forward with
their app development. strongly recommend libraries like Sencha Touch, jQuery Mobile and
iScroll especially as doing scrolling from scratch took me more than 50% of total development
time which is ridiculous!]
TODO useful references

PhoneGap by Dissection

  • 2.
    PhoneGap by Dissection Myfirst 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
  • 3.
    Dedicated to allthe hard-working girls and boys in the free and open source software communities.
  • 4.
    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
  • 5.
    CONTENTS 14.That’s all folks!. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
  • 6.
    1. Introduction This bookis 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:
  • 7.
    Introduction 2 someMethod(); File namesand 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.
  • 8.
    2. What you’llneed 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!
  • 9.
    3. What isPhoneGap 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.
  • 10.
    4. Getting started Thereare 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
  • 11.
    Getting started 6 Thisinstalls 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.
  • 12.
    Getting started 7 .. Howmany 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
  • 13.
    Getting started 8 4.2The 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).
  • 14.
    5. Quick run-throughof 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>
  • 15.
    Quick run-through ofthe 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):
  • 16.
    Quick run-through ofthe 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 */ }
  • 17.
    Quick run-through ofthe 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);
  • 18.
    Quick run-through ofthe 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:
  • 19.
    Quick run-through ofthe 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!).
  • 20.
    6. First thingsfirst: 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:
  • 21.
    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:
  • 22.
    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:
  • 23.
    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:
  • 24.
    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:
  • 25.
    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:
  • 26.
    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
  • 27.
    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
  • 28.
    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
  • 29.
    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:
  • 30.
    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...*/
  • 31.
    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.
  • 32.
    7. First thingsfirst: 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:
  • 33.
    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:
  • 34.
    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:
  • 35.
    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:
  • 36.
    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).
  • 37.
    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:
  • 38.
    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:
  • 39.
    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:
  • 40.
    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!:
  • 41.
    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:
  • 42.
    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!:
  • 43.
    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:
  • 44.
    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:
  • 45.
    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,
  • 46.
    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)
  • 47.
    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.
  • 48.
    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:
  • 49.
    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 }
  • 50.
    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:
  • 51.
    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.
  • 52.
    8. The Searchtab 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:
  • 53.
    The Search tab48 <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:
  • 54.
    The Search tab49 #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!)
  • 55.
    The Search tab50 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:
  • 56.
    The Search tab51 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
  • 57.
    The Search tab52 .. 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:
  • 58.
    The Search tab53 //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:
  • 59.
    The Search tab54 //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:
  • 60.
    The Search tab55 //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:
  • 61.
    The Search tab56 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).
  • 62.
    The Search tab57 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:
  • 63.
    The Search tab58 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:
  • 64.
    The Search tab59 //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
  • 65.
    The Search tab60 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.
  • 66.
    The Search tab61 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:
  • 67.
    The Search tab62 //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.
  • 68.
    The Search tab63 .. 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
  • 69.
    The Search tab64 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).
  • 70.
    The Search tab65 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:
  • 71.
    The Search tab66 //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); } }
  • 72.
    The Search tab67 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
  • 73.
    The Search tab68 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/
  • 74.
    The Search tab69 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:
  • 75.
    The Search tab70 //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.
  • 76.
    The Search tab71 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:
  • 77.
    The Search tab72 //----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
  • 78.
    The Search tab73 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',
  • 79.
    The Search tab74 '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:
  • 80.
    The Search tab75 //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:
  • 81.
    The Search tab76 //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:
  • 82.
    The Search tab77 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:
  • 83.
    The Search tab78 //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.
  • 84.
    The Search tab79 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:
  • 85.
    The Search tab80 //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
  • 86.
    The Search tab81 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:
  • 87.
    The Search tab82 //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.
  • 88.
    The Search tab83 .. 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
  • 89.
    The Search tab84 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; }
  • 90.
    The Search tab85 //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');
  • 91.
    The Search tab86 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
  • 92.
    The Search tab87 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:
  • 93.
    The Search tab88 //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]
  • 94.
    The Search tab89 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!).
  • 95.
    9. The Discovertab 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:
  • 96.
    The Discover tab91 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
  • 97.
    The Discover tab92 (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:
  • 98.
    The Discover tab93 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
  • 99.
    The Discover tab94 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:
  • 100.
    The Discover tab95 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:
  • 101.
    The Discover tab96 Figure 42. Flow when clicking “reply” from timeline widget So it already sorta kinda works in two different ways, but each has its issues. We are going to let this slide for the first release of the app, but this issue really needs to be addressed. Perhaps the widget itself has some useful customisation options? Or maybe links could be made to look non-linky by some CSS in our app? Or JavaScript click catching? 9.2 Extra credit challenges Solutions not provided. Try to add: 1. Disabling of clickable intents on tweets in the timeline widget OR.. 2. ..Correct functioning of the clickable intents when the Twitter app is selected (and not the browser) 3. Instead of simply displaying “(network connection required)”, use the Connec- tion API plugin to properly detect if the device is currently online or not. If it’s not online, then do something to inform the user that the Discover tab needs the device to be online.
  • 102.
    10. The Writetab 10.1 Layout and interface Our third and final tab now which is Write. This is more complex than the previous Discover tab, but slightly quicker to implement than the first Search tab - mostly due to not having any scrolling woes to care about. The write tab is going to be a little scratchpad area for the user to practice writing Japanese phonic characters. We’ll present a random character and then an empty canvas for the user to finger draw the character. A wireframe might look like this: Figure 43. Quick wireframe of the Write tab layout We simply present a character and a space for them to practice drawing it in. Let’s start with the markup, and a dollop of CSS, first. Edit the write div of index.html to look like this:
  • 103.
    The Write tab98 div id=write class=current p style=text-align:center;Write this character: span style=font-size:2em; id=char-to-write/span span id=char-explanation/span /p canvas id=paper width=300 height=300/canvas br button type=button id=canvas-clear style=width:45%; margin-right:1%; float:ri ght; img src=img/paste.png Clear /button button type=button id=canvas-new style=width:45%; margin-left:1%; img src=img/file.png New character /button /div (Note that, like we did with the Discover tab, we’ve temporarily made this content div - and it’s corresponding tab - class=current to speed up our development a tad.) We have an explanatory paragraph for our random character, and placeholders for both the character to write (in a larger font) and its explanation. Then comes the magic and the main focus of this chapter, the whizz-bang HTML5 canvas ele- ment which you may or may not have seen before. It’s an element that allows for programmatic and interactive display and manipulation of simple 2D graphics. It’s kind of like Microsoft Paint but in the browser and you have to program it with JavaScript. (It’s actually waaaaay better than I’ve just made it sound!) If you’ve got alarm bells ringing because we appear to have hard-coded the canvas dimensions (300px x 300px) then you are right. We will come back and improve this shortly. We end with two buttons, a Clear button which we want to erase the user’s scribbling so far, and a New character button which will display a new random Japanese character to draw. Well if you run all of this, it looks like:
  • 104.
    The Write tab99 Figure 44. Initial Write tab appearance Clearly the canvas needs a bit of styling. Add a style for the canvas element to index.css thus: canvas { border:1px solid grey; background-color:#ffa; /*Post-It yellow*/ margin-left:auto; /*these two lines will*/ margin-right:auto; /*centre the element horizontally*/ display:block; } Running looks like this now: Figure 45. Write tab appearance with styled canvas
  • 105.
    The Write tab100 Better! 10.2 Filling the screen Hmmm, but Android devices come in all shapes and sizes. What to do about the size of the canvas? We would want, ideally, the biggest square (Japanese characters tend to be rather squareish) that would fit on the screen at that time - for portrait or lanscape. We’ll take an approach like this: 1. Put the two buttons at the bottom of the screen 2. Make a containing div for the canvas that fills 100% of its available width and height (the height under “Write this character” and above the buttons, then the full width of the screen) 3. Set the size of the canvas to be the biggest square that will fit in this container. Centred in the container 4. When the device is rotated, we will have to resize the canvas to be the biggest square in the newly created container size OK, tackling [1] and [2] first, we need a new layout. We are going to do the same thing we did for the app’s header, content and footer; position them absolutely relative to parent. But this time the parent will be our .japxlate_app div. Edit the write div in index.html to look like this: div id=write class=current p id=write-introWrite this character: span style=font-size:2em; id=char-to- write/span span id=char-explanation/span /p div id=write-canvas-container !--canvas id=paper width=300 height=300/canvas-- /div div id=write-buttons button type=button id=canvas-clear style=width:45%; margin-right:1%; floa t:right; img src=img/paste.png Clear /button button type=button id=canvas-new style=width:45%; margin-left:1%; img src=img/file.png New character /button /div /div We’ve given the intro paragraph an id. We’ve commmented the canvas out for the time being but we’ve put it in a container div of #write-canvas-container. The buttons have also been placed in a containing div of #write-buttons. Now we need to position and size these elements such that the intro paragraph will be right at the top, the buttons will be right at the bottom, and the canvas container will fill all the space inbetween. Add these CSS rules to index.css:
  • 106.
    The Write tab101 #write-intro { position:absolute; top:0; /*absolute top of .japxlate_app*/ height:40px; /*make arbitrarily big enough for our 2em character*/ width:100%; margin-bottom:0; /*so #write-canvas-container is flush*/ margin-top:10px; /*so we aren't directly under the navigation tabs*/ text-align:center; /*centre text horizontally*/ /*background-color:green;*/ } #write-canvas-container { position:absolute; top:50px; /*make flush with #write-intro*/ bottom:40px; /*stop 40px up from the botton of .japxlate_app*/ width:100%; } #write-buttons { position:absolute; bottom:0; /*absolute bottom of .japxlate_app*/ height:40px; /*make flush with bottom of #write-canvas-container*/ width:100%; } We simply make write-intro and write-buttons a little bit bigger than they need to be, and set canvas container to fill the remaining space. We set margin-top of #write-intro to 10px so that the intro paragraph text is not too close to the tabs and so that we know the top of #write- canvas-container is 50px. We have previously set padding-top of .japxlate_app to 1em but this is obliterated with the position:absolute and top:0 of #write-intro. (So yes, we’ve set a default top padding of 1em on .japxlate_app but only used it in the Search tab as we positioned over it on the Discover tab and this tab!). If you like you can confirm the size and shape of these divs by setting a different background-color in each of the CSS rules and then changing the size of desktop Chrome or rotating your device. For [3] we need to programatically get the dimensions of #write-canvas-container, work out the biggest square that will fit in those dimensions (possibly trimming a bit off so our canvas isn’t too close to the buttons etc), and then dynamically add the appropriate canvas element into #write-canvas-container - centreing it. For [4] we need to catch a device rotation event and then do the steps for [3] again. Create a file called canvas.js in assets/www/js. Include this JavaScipt file from the bottom of index.html (above the include for index.js). We are going to implement a function in canvas.js called adjustCanvas() which will be our step [3]. adjustCanvas() looks like this:
  • 107.
    The Write tab102 //adjust - creating if necessary - the canvas element function adjustCanvas() { var container = document.getElementById('write-canvas-container'); var style = window.getComputedStyle(container); var width = parseInt(style.width); var height = parseInt(style.height); var smallestDim = width; //smallest dimension is width (= portrait) if(height width) //ie. landscape { smallestDim = height; } //invisible frame around canvas so it's not flush with buttons etc var frameGap = 15; smallestDim -= (frameGap * 2); //gap at top and bottom (left and right) var canvas = null; //we proceed to get or create this //element existence check var firstTime = !document.getElementById('paper'); if(firstTime) //create canvas element with correct id { canvas = document.createElement('canvas'); canvas.id = 'paper'; canvas.style.position = 'relative'; //so we can top the canvas down } else //get existing canvas element { canvas = document.getElementById('paper'); } //size and position the canvas canvas.width = smallestDim; canvas.height = smallestDim; canvas.style.top = frameGap + 'px'; //add canvas (as child of container) if first time if(firstTime) { container.appendChild(canvas); } }
  • 108.
    The Write tab103 Wow, this is our longest piece of JavaScript so far. With the first two lines we get the container element for the canvas, and its style. We then save the width and height of the container. We get the smallest dimension of the container (which will be the squared size of our canvas) by assuming the container is portrait shaped. If container height is less than container width, we say it is landscape shaped. (A perfectly square container will be covered by the portrait assumption which will be fine as any of its side measurements is fine to use in that case.) We then put an imaginary frame around the canvas of 15 pixels so that the bottom of the canvas is not flush with our buttons. Thinking ahead to step [4], it would be nice if this function to set up the canvas initially could also be used to adjust the canvas for a device rotate. We tackle this with the concept of “first time”. Basically, if the function is happening for the first time - ie. the canvas does not exist - then it must create the canvas. If not the first time then it simply needs to alter the existing canvas. We action this by: var firstTime = !document.getElementById('paper'); Which relies on document.getElementById('someId') returning boolean false if an element with the id of someId does not exist on the page. So we get or create the canvas ele- ment and set its size to the smallest dimension of the container. Minus our frame gap of course. The first time around we have to insert the created canvas element into the DOM (document.createElement('elementname'); creates the element in memory only) which we do with container.appendChild(canvas);. Go ahead and delete the line: canvas.style.position = 'relative'; and put that in the canvas{} rule in index.css as that makes more sense. Next, mosey on over to firstLoadForTab_Write() in japxlate.js and add a call to adjustCanvas() thus: function firstLoadForTab_Write() { console.log('first load for write tab'); adjustCanvas(); //create canvas element of correct size global_pagesLoaded.write = true; } Remember firstLoadForTab_Write() is our one-off initialiser for the Write tab and so running the app now looks like:
  • 109.
    The Write tab104 Figure 46. canvas now fills available space Pretty good! You can test that the canvas fills the available space by closing the page, resizing the browser and loading the page again (desktop Chrome) or closing the app, rotating your phone, reopening the app (device). OK, that was actually the easy bit! Our next step is [4] and this is a bit fiddly as we need to figure out how to detect a device rotation. Well, the proper way is to catch the “orientationchange” event (of the window object) with a handler. (Interestingly there is no PhoneGap API to do this.) Orientationchange will fire for each and every orientation change of the device. And then in that handler you can use the window.orientation property to work out the device orientation. Window.orientation will be 0 meaning portrait, 90 meaning landscape (top of phone pointing right) or -90 meaning landscape (top of phone pointing left). These values represent the number of degrees the phone has been rotated from the resting - or zero - position which is portrait. Android does not allow the screen to be rotated upside-down and so there is no 180 value (phones only?). As you would expect, our desktop Chrome browser doesn’t support the orientationchange event. (You can try to spin your monitor around but don’t blame me if you wreck anything!). Keeping our spirit of making the app work as an app and in the desktop Chrome, we are going to do something a little different. (Although orientationchange is the “correct” way to do it.) The closest thing to orientationchange on a desktop browser, and something that will also work on our WebView browser, is the “resize” event of the window object. This event fires for every resize - big or small - of the browser window. This includes the resize that our device will do to the WebView when we rotate the device. OK, let’s get the ball rolling (or should that be rotating? LOL). Stick a call to configureCanvasRotationAdjustment(); at the bottom of receivedEvent() in index.js. We define this function in canvas.js:
  • 110.
    The Write tab105 //device rotation handler function configureCanvasRotationAdjustment() { window.addEventListener('resize', adjustCanvas, false); } Run this and you can see that the canvas resizes when you rotate the device OR when you resize the desktop Chrome. But there’s a problem, if you click on a different tab (not Write), rotate the device / resize the browser and then click back on the Write tab, you get a tiny canvas like this: Figure 47. canvas can become too small What’s happening is this: Our onresize handler (adjustCanvas()) triggers when the browser resizes, regardless of whichever tab we are on. The canvas container will not be visible when not on the Write tab because of our whole tabbing mechanism. But adjustCanvas() will still get called and create or adjust the canvas. It seems like .getComputedStyle() picks up the canvas container smallest dimension as 100px when it is not visible. This resizes the canvas to a tiny size - it just isn’t visible until you click the Write tab. Remember we put adjustCanvas() in firstLoadForTab_Write()? This means that when we go back to the Write tab after clicking another tab, adjustCanvas() is not called. One solution would be to somehow only action the resize handler when on the Write tab. But what we will do instead is move the call to adjustCanvas() out of firstLoadForTab_Write() and into onclickForTab_Write() such that the canvas is adjusted on every clicking of the Write tab. So re- move adjustCanvas() from firstLoadForTab_Write() and stick it in onclickForTab_Write() so it looks like this:
  • 111.
    The Write tab106 function onclickForTab_Write() { console.log('click on write tab'); if(!global_pagesLoaded.write) { firstLoadForTab_Write(); } adjustCanvas(); //adjust - creating if necessary - the canvas element } Run this and the problem has been resolved. Nice. Phew, so the canvas display and layout is pretty hot and tasty right now and should be appropriate for any device that the app runs on. 10.3 Displaying a random character Now, even before we get to the New character and Clear buttons, we need to present a random Japanese character for the user to draw. And of course we need to make finger movements on the canvas actually write something! Put a call to the soon-to-be-implemented doNewChar() at the end of onclickForTab_Write() (so that every click on the Write tab will present a new character to practice) such that it looks like this: function onclickForTab_Write() { console.log('click on write tab'); if(!global_pagesLoaded.write) { firstLoadForTab_Write(); } adjustCanvas(); //adjust - creating if necessary - the canvas element //get and display a random Japanese character to practice writing doNewChar(); } doNewChar() is going to get a random Japanese character, and display it above the canvas for the user’s reference. So before we implement doNewChar(), we need a slight detour - we need the function to get a random Japanese character! We will put this in, you guessed, linguistics.js:
  • 112.
    The Write tab107 //return a random (non-chiisai, non-obsolete) hiragana or katakana //RETURNs object like {char:'', romaji:'ku', type:'hiragana'} function getRandomKana() { //indices to ignore from coreHiragana: //64,65,68,=74 //so this is indices of coreHiragana of chars that we WANT to practice //(ie. not chiisai or obsolete): var coreIndices = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 66, 67, 69, 70, 71, 72, 73 ]; //get one of above indices at random var index = coreIndices[Math.floor(Math.random() * coreIndices.length)]; //default to hiragana... var char = coreHiragana[index]; //use our random index var type = 'hiragana'; //...but have a 50% chance of returning katakana if(Math.random() 0.50) { char = hira_to_kata(char); type = 'katakana'; } //return a useful object return {char:char, romaji:kana_to_romaji(char), type:type}; } This again might be something that’s best ignored and treated as a black box, but basically we cherry pick a phonetic character from coreHiragana based on some desired indices (to give us a full size and non-obsolete character). We then convert that character into katakana 50% of the time. We return an object with the character itself and some metadata. Let’s use this function in doNewChar() which we define in canvas.js:
  • 113.
    The Write tab108 //Get and display (with explanation) a random Japanese character //to practice writing function doNewChar() { var randomKana = getRandomKana(); document.getElementById('char-to-write').innerHTML = randomKana.char; document.getElementById('char-explanation').innerHTML = '(' + randomKana.romaji + ' in ' + randomKana.type + ')'; } Running the app now looks like this: Figure 48. Random Japanese character with metadata is displayed Pretty cool! We get the random character to practice writing in a nice big font, the English spelling / pronunciation and the type of script it’s from. Wait, I’ve just had a good idea. How about, before we let them start writing, if we flash up the character on the canvas, filling the canvas and then fading out for them to start copying it! That’s gonna be awesome! Note that this next bit (an initial, fading character on the canvas) is going to be somewhat difficult and involves recursive JavaScript. We’ll want to do this character fading for every new character that we present. So a good place to call our fading function will be from doNewChar() in canvas.js. Add this as the last line in doNewChar():
  • 114.
    The Write tab109 //put character on canvas and fade to nothing (starting at 1 opacity) //our trademark #990000 red is rgb(153, 0, 0) fadeCharOnCanvas(randomKana.char, 153, 0, 0, 1, 1, 100, 10); We call fadeCharOnCanvas() with a lot of parameters. Let’s implement fadeCharOnCanvas() - also in canvas.js - right now. It’s a biggie because it uses a few helper functions and variables that we’ll implement shortly. So don’t be scared if you see something that hasn’t been referenced yet! OK, implement fadeCharOnCanvas() to look like this: //Animate a fade out of a single (Japanese) character on the canvas. //Starting from opacity of startAlpha and stepping down to zero opacity function fadeCharOnCanvas(char, startR, startG, startB, startAlpha, thisAlpha, msDelay, frameCount) { //calc the step down amount var dec = startAlpha / frameCount; //what will the *next* opacity be? var nextAlpha = thisAlpha - dec; //console.log('thisAlpha:' + thisAlpha + ' -- nextAlpha:' + nextAlpha); //dues to floating point rounding we prob won't reach exactly zero //BUT we want exactly zero for the char to disappear //SO if thisAlpha is on the last frame, force to zero if(thisAlpha = (startAlpha - ((frameCount - 1) * dec))) { //console.log('last frame reached'); thisAlpha = 0; } clearCanvas(); //else we are drawing a lighter char over a darker one! //console.log(global_canvasElement.width); var fontSize = parseInt(global_canvasElement.width * 0.833); //console.log(fontSize); global_canvas.font = 'normal ' + fontSize + 'px serif'; global_canvas.fillStyle = 'rgba(' + startR + ',' + startG + ',' + startB + ',' + th isAlpha + ')'; //rgb alpha global_canvas.fillText(char, parseInt(global_canvasElement.width * 0.05), parseInt( global_canvasElement.width * 0.78)); if(nextAlpha 0) { //console.log('last frame drawn - exiting'); return; }
  • 115.
    The Write tab110 //recurse! setTimeout(function(){fadeCharOnCanvas(char, startR, startG, startB, startAlpha, ne xtAlpha, msDelay, frameCount)}, msDelay); } The canvas API lets us write text in any colour and at any opacity. We set the opacity using what’s called an alpha value; one meaning fully opaque and zero meaning fully transparent. In a nutshell, we simply draw the character on the canvas a bunch of times and increase the transparency at each step. OK, let’s have a fuller explanation… We accept, in order of appearance, these parameters: 1. char - the single Japanese character to draw 2. startR - red element of colour to use for drawing (integer 0 - 255) 3. startG - green element of colour to use for drawing (integer 0 - 255) 4. startB - blue element of colour to use for drawing (integer 0 - 255) 5. startAlpha - opacity used to draw the first frame (float 0.0 - 1.0) 6. thisAlpha - opacity used to draw the current frame (float 0.0 - 1.0) 7. msDelay - the delay, in milliseconds, between each frame 8. frameCount - how many frames to take to get down to zero opacity (Note that startR, startG and startB don’t actually change value at any step of the recursion.) We save in dec the amount we have to decrement the starting opacity (startAlpha) by each step to reach zero opacity after the specified number of frames (frameCount). We then use dec to work out what the opacity of the next frame will be. We’ll use this value a bit later on. Next we have a workaround for the vagaries of floating point arithmetic. Basically, due to rounding, we might not reach an opacity of exactly zero on our last frame. So we do an if check here to see if we are more-or-less on the last frame now (ie. (frameCount - 1) frames have already happened). If we are, we force the current opacity to zero. Next, and before we draw anything, we clear the canvas. We will define clearCanvas() shortly. Next we calculate fontSize to make the character best fill the canvas. During development I saw that for a 300px by 300px canvas, a font size of 250px was best. This is the equivalent of the font size being 83.3% of the canvas size. Trial-and-error with some different canvas sizes told me that this percentage just works. global_canvasElement is the canvas DOM element which we need to have available when this function is called. Next we use the font property of global_canvas to set the font size we just calculated. global_- canvas is the drawing context of our canvas element which we need to have available when this function is called. We cover this shortly so don’t worry. Next we use an rgba (“red, green, blue, alpha”) value to set the global_canvas.fillStyle property. This sets the colour and opacity that our character will get drawn with. Then we call global_canvas.fillText() which actually writes our character on the canvas. The second argument is the canvas x coordinate where you want the (left edge of the) text to start
  • 116.
    The Write tab111 drawing. The third argument is the canvas y coordinate where you want the (bottom baseline of the) text to be. Again by trial-and-error I saw it was best for the x coordinate to be 5% of the canvas total width, and the y coordinate to be 78% of the canvas total height. OK, that’s one frame drawn, but we are still in the function and so we need to exit the function if we have just drawn the last frame. That’s what we do with the next if check. Last but not least, if we are still in the function then we have another frame to draw. We make the function call itself (recursion) at the end. But this should only happen after the specified delay (msDelay) and so we use JavaScript’s builtin setTimeout() method (of the window object) to call ourself after the delay. setTimeout() works like setTimeout(someCallableThing, delayInMilliseconds);. We simply call ourself with the updated value for nextAlpha - all the other arguments are the same. Great! But we still need to define clearCanvas() and initialise global_canvas and global_- canvasElement. Let’s do the globals first, define them at the top of canvas.js like this: //The drawing context of our canvas var global_canvasElement; //Our canvas DOM element var global_canvas; Nothing difficult there. Now, again in canvas.js, define initialiseCanvas() thus: //Save our global canvas variables function initialiseCanvas() { //set the globals global_canvasElement = document.getElementById('paper'); global_canvas = global_canvasElement.getContext('2d'); } Good old document.getElementById() is used to save the canvas DOM element into global_- canvasElement. For global_canvas we call getContext('2d') on our canvas DOM element. This is a builtin method of the HTML5 canvas object and will return an API for 2D drawing. (Yes, there’s a 3D API - getContext('webgl') - but it won’t work in as many browsers as the 2D API.) The question now is where to call initialiseCanvas()? If you’re thinking that we need to call it once and only once, then you’re thinking the same as me. However, during development I noticed an obscure quirk with the canvas drawing context whenever the canvas was resized (ie. we rotate our device): the canvas drawing context would reset! With this in mind we need to call initialiseCanvas() every time the canvas is adjusted. This is actually easy to do and simply involves adding: initialiseCanvas(); as the very last line of adjustCanvas(). Just clearCanvas() left now. Stick it in canvas.js and it goes like:
  • 117.
    The Write tab112 //Clear the canvas function clearCanvas() { global_canvas.clearRect(0, 0, global_canvasElement.width, global_canvasElement.heig ht); } We use the clearRect() canvas API method to clear a rectangle on the canvas. The rectangle we clear starts at (0, 0) in canvas space and the width and height matches that of the canvas itself. This clears the entire canvas. All done! You should now get the Japanese character fading down on the canvas for each new character! 10.4 Finger doodling Right, on to the biggie now which is making something draw on the canvas when the user moves their finger in there. Do you remember our work on finger scrolling for the Search tab? Remember we got it working on the device first then we simulated finger presses - using mouse events - for debugging in desktop Chrome? Well, let’s do it the other way round this time. Let’s get canvas writing working on desktop Chrome first. When the mouse is down in the canvas, and then the mouse is moved, we want to leave a “trail” on the canvas as if drawing with a calligraphy brush or what-have-you. I mentioned earlier that HTML5 canvas is somewhat like MS Paint in the browser. Like MS Paint, it has a line drawing API where we can start the “brush” at a certain coordinate, and then paint a line from there to any other coordinate. And, like we did for simulated finger scrolling, we are going to need a global variable to track the mouse downness. In fact, we’ll recycle the same global variable (so yes, it might not logically belong in a file called search_interface.js). You remember our initialiseCanvas() in canvas.js which is where we stored the canvas in global variables just after creating it for the first time. That seems like a good place to put the event listeners we’re going to need for our mouse drawing. Add three addEventListener() calls - on the canvas element - at the bottom of initialiseCanvas() such that it now looks like this: function initialiseCanvas() { //set the globals global_canvasElement = document.getElementById('paper'); global_canvas = global_canvasElement.getContext('2d'); //simulated touch (ie. mouse) dragging for writing pad global_canvasElement.addEventListener('mousedown', mousedownForCanvas, false); global_canvasElement.addEventListener('mousemove', mousemoveForCanvas, false); global_canvasElement.addEventListener('mouseup', mouseupForCanvas, false); }
  • 118.
    The Write tab113 You’ve guessed it, we’re going to define mousedownForCanvas(), mousemoveForCanvas() and mouseupForCanvas() right now; also in canvas.js: //Mousedown event handler for canvas - set now drawing state function mousedownForCanvas(event) { global_mouseButtonDown = true; event.preventDefault(); } //Mousemove event handler for canvas - draw on canvas function mousemoveForCanvas(event) { var x, y; // Get the mouse position relative to the canvas element. //(ie. the mouse position IN CANVAS SPACE) if (event.offsetX || event.offsetX == 0) { // Opera and Chrome x = event.offsetX; y = event.offsetY; } //This event handler works like a drawing pencil which tracks the mouse //movements. We start drawing a path made up of lines. if(global_mouseButtonDown) { doDrawOnCanvas(x, y); //our canvas API magic } } //Mouseup event handler for canvas - unset now drawing state function mouseupForCanvas(event) { //console.log('mouseup event on canvas'); global_mouseButtonDown = false; global_startedDrawing = false; event.preventDefault(); } mousedownForCanvas() simply sets our recycled global_mouseButtonDown to true. In mousemoveForCanvas() we receive the mouse event and, if the mouse button is down, call doDrawOnCanvas() with canvas space x and y coordinates. Specifically we pass it offsetX and offsetY of the mouse event. In Chrome (and also Opera but definitely not Firefox), this is the xy coords of the mouse event but offset to be in element space. Element space meaning the very top-left of the element is x=0, y=0 and so on. Why call doDrawOnCanvas() and not just do the canvas drawing logic in the mousemove handler? Well it’s because, like we did for search results scrolling, we want to reuse the same
  • 119.
    The Write tab114 logic for actual finger drawing and simulated finger drawing with the mouse. We’ll get on to doDrawOnCanvas() in a moment. Finally in mouseupForCanvas() we set global_mouseButtonDown to false. We also set global_- startedDrawing to false but we haven’t defined that yet. Let’s define it first then I’ll explain. Stick: //Are we already drawing on the canvas? var global_startedDrawing = false; at the top of canvas.js with the other globals. We need to keep track of this because, as we will see shortly, with canvas API we have two very distinct steps of drawing lines; moving the brush to a certain point to start the path and then actually moving the brush from that point. So most of the magic happens in our doDrawOnCanvas() which we will define right now, also in canvas.js: //Universal canvas line plotter. for onmousemove etc function doDrawOnCanvas(canvasX, canvasY) { //This works like a drawing pencil which tracks the mouse / touch //movements. We draw a path made up of lines. if(!global_startedDrawing) //first time { global_canvas.beginPath(); global_canvas.moveTo(canvasX, canvasY); global_startedDrawing = true; } else { global_canvas.lineTo(canvasX, canvasY); global_canvas.stroke(); } } We expect to receive x and y coordinates in canvas space, which is conveniently provided by .offsetX and Y on the mousemove event. (The offsetX and Y properties will contain the screen coordinates of the mouse converted into element space such that if the mouse is in the very top-left of the element - our canvas - the offsetX and Y will be 0,0.) For the first time that drawing has started, we call a couple of canvas API methods to get ready for drawing. We call beginPath() which starts a new path. A path is basically a line or curve that we draw onto the canvas. We then call moveTo() which positions the “brush” but does not actually paint anything. We then, for subsequent mousemoves or finger drags set global_startedDrawing to true so that the next bit can happen… Which is simply to call two more canvas API methods. We call lineTo() with the updated mouse / finger position (again in canvas space) which will move the brush from its path starting point
  • 120.
    The Write tab115 to the point specified. Note that even this will not paint anything on the canvas. For that we need to call stroke() which will actually follow the path and put the “ink” down. Give it a whirl in desktop Chrome! You should be able to paint on the canvas using the mouse! Figure 49. Canvas painting with mouse Hmmm, but that thin black line doesn’t feel so great. Let’s fatten it out and make it our signature red. HTML5 canvas exposes a bunch of properties that we can tinker with to affect line drawing style. The right place to change these settings feels like our one-off canvas initialiser of initialiseCanvas(). Add two lines to the bottom so it looks like: function initialiseCanvas() { //set the globals global_canvasElement = document.getElementById('paper'); global_canvas = global_canvasElement.getContext('2d'); //simulated touch (ie. mouse) dragging for writing pad global_canvasElement.addEventListener('mousedown', mousedownForCanvas, false); global_canvasElement.addEventListener('mousemove', mousemoveForCanvas, false); global_canvasElement.addEventListener('mouseup', mouseupForCanvas, false); //line drawing style global_canvas.strokeStyle = '#990000'; //our trademark red global_canvas.lineWidth = 10; } We can set the colour that stroke() will use with .strokeStyle. We can set the line width with, yes, .lineWidth. Try it!:
  • 121.
    The Write tab116 Figure 50. Line angles and edges somewhat jaggedy Much better! The red looks great and it feels nice and fat like a calligraphy brush. But, and this is much easier to notice when you’re actually drawing with it and not looking at this screenshot, there’s something not quite right. The drawn line seems to have some quite sharp edges and generally looks jaggedy. Digging deeper into the canvas API, we have some interesting properties to change the brush drawing style. The lineJoin property of the canvas (http://www.html5canvastutorials.com/tutorials/html5-canvas-line-joins is a useful page) spec- ifies how we want to draw the “join” between two lines which in our case basically means the point where we change the direction of drawing with our brush. The default is “miter” which is a very sharp join. You can see this on the above figure. The remaining options are “bevel” and “round”. We’ll go for round as this most resembles what a calligraphy brush would do. There is also the “lineCap” property (http://www.html5canvastutorials.com/tutorials/html5- canvas-line-caps is a useful page) which is how to draw the end of the line. As you can see from the above figure, the default is “square”. For that calligraphy feel, let’s make this also “round”. So add to the end of initialiseCanvas() so it looks like this: function initialiseCanvas() { //set the globals global_canvasElement = document.getElementById('paper'); global_canvas = global_canvasElement.getContext('2d'); //simulated touch (ie. mouse) dragging for writing pad global_canvasElement.addEventListener('mousedown', mousedownForCanvas, false); global_canvasElement.addEventListener('mousemove', mousemoveForCanvas, false); global_canvasElement.addEventListener('mouseup', mouseupForCanvas, false); //line drawing style global_canvas.strokeStyle = '#990000'; //our trademark red global_canvas.lineWidth = 10; global_canvas.lineCap = 'round'; //dat calligraphy feel global_canvas.lineJoin = 'round'; //dat calligraphy feel } Try running the app now and it draws like this:
  • 122.
    The Write tab117 Figure 51. Line angles and ends have a rounder feel Awesome! I feel warm and fuzzy inside. Right, let’s quickly get our bottom buttons in the bag and then we’ll get our calligraphy brush working with our sticky fingers on our actual device. Okey dokey, we’re going to need some event handlers to do the appropriate things for our New character and Clear buttons. Stroll over to index.js and whack a call to configureCanvasButtons(); at the end of receivedEvent(). receivedEvent() will look like this now: // 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(); configureSearchButton(); configureSearchInput(); configureSearchTouchScrolling(); configureSearchMouseScrolling(); configureCanvasRotationAdjustment(); configureCanvasButtons(); }, We’ll define configureCanvasButtons() in canvas.js thus:
  • 123.
    The Write tab118 //New character and Clear buttons function configureCanvasButtons() { document.getElementById('canvas-clear').addEventListener('click', clearCanvas, fals e); document.getElementById('canvas-new').addEventListener('click', doNewChar, false); } And that’s it! The Clear button simply calls our existing clearCanvas() function, and the New character button simply calls our existing doNewChar() function. Try it! The write tab is much more fun now! Right, the remaining matter is the important one of getting drawing to work with finger moves on the device itself. We are happy with how it works in desktop Chrome now so let’s move forward and think about the device. We isolated the doDrawOnCanvas() function and it is ready to accept canvas coordinates from any event, not just mouse events. Just like with search results scrolling, the events in question are touchstart, touchmove and touchend. These will somewhat correlate with the mousedown, mousemove and mouseup handlers (respectively) that we’ve just implemented. In initialiseCanvas() in canvas.js, go ahead and insert these three lines to add our touch event handlers. Insert them after the mouse event handlers: //touch dragging for writing pad global_canvasElement.addEventListener('touchstart', touchstartForCanvas, false); global_canvasElement.addEventListener('touchmove', touchmoveForCanvas, false); global_canvasElement.addEventListener('touchend', touchendForCanvas, false); Great, now let’s define these handlers; again in canvas.js: //Touchstart event handler for canvas function touchstartForCanvas(event) { //console.log('touchstart event on canvas'); event.preventDefault(); } //Touchmove event handler for canvas - draw on canvas function touchmoveForCanvas(event) { var touchobj = event.changedTouches[0]; //reference first touch point for this event //where, in canvas space, has been touched? var x, y; x = touchobj.offsetX; y = touchobj.offsetY;
  • 124.
    The Write tab119 doDrawOnCanvas(x, y); } //Touchend event handler for canvas - unset now drawing state function touchendForCanvas(event) { //console.log('touchend event on canvas'); global_startedDrawing = false; event.preventDefault(); } We don’t actually do anything in the touchstart handler, but we’ll keep it in case we do need to do anything in future. In the touchmove handler we get the first touch point and, like we did for mousemove, pass its offsetX and Y to doDrawOnCanvas(). Finally the touchend handler simply sets the global “started drawing” state to false; ready for the next scribble. OK, so run this on your device and, hmmmm, finger drawing does not work! What gives? The problem is that the touch object we get from the touchmove event does not have offsetX or Y properties! Nor does the touchmove event itself! This is officially A Very Annoying Thing. I think the JavaScript implementors are missing a bit of a trick here honestly. Without offsetting the touch object coordinates, they will be screen coordinates and will not correlate to canvas space. They will basically be too big to point to anywhere meaningful in the canvas. The good news is that every DOM element under body exposes some offset properties. Specifically we have .offsetParent which points to an element’s parent element, and we have .offsetLeft and .offsetTop which tell us, in pixels, how far away from the top-left of the parent the top-left of the element is. The bad news is that most elements, in particular our canvas, have multiple parents. We are going to have to programatically loop up through an element’s parents and tot up the offsets to work out a true screen offset of an element. It’s actually a trivial function and let’s put it in canvas.js: //Get DOM element position on page function getPosition(obj) { var x = 0, y = 0; if (obj.offsetParent) { do { x += obj.offsetLeft;
  • 125.
    The Write tab120 y += obj.offsetTop; obj = obj.offsetParent; } while(obj); } return {'x':x, 'y':y}; }; We accept a DOM element in obj. We start the x and y values - our offset - at zero. Then, if we have an .offsetparent on obj (if not then it is body and will have no parent) we loop (at least once) cumulatively adding .offsetLeft to x and .offsetTop to y. We loop as long as there is a parent up the chain. We return the coordinates in a little object. Right, with this we can go back and fix touchmoveForCanvas(). Edit touchmoveForCanvas() to look like this: function touchmoveForCanvas(event) { var touchobj = event.changedTouches[0]; //reference first touch point for this event //where, in canvas space, has been touched? var x, y; var canvasOffset = getPosition(global_canvasElement); //why no offset x and y for if it's a touch event? :-( x = touchobj.screenX - canvasOffset.x; y = touchobj.screenY - canvasOffset.y; doDrawOnCanvas(x, y); } Run this on your device. It works! OMG, we’ve nailed it, we’ve finished all the tabs! Crack open the beers at this point ;-) Just two more easy things. Let’s have a splashscreen that displays while we are waiting for the app to start. Also, we’re going to need something other than PhoneGap’s default launcher icon! 10.5 Extra credit challenges Solutions not provided. Easy Prevent the current character from being displayed again when clicking the New character button
  • 126.
    The Write tab121 Medium The way we have centred the big fading character on the canvas is slightly ridiculous. Use the canvas API to properly centre the character on the canvas [NOTETOSELF find the good link for this] Difficult Technically what we are doing here for finger doodling is following the mouse or finger and, underneath it, creating a multi-sectional line. We aren’t actually simply placing a pixel under the mouse or finger. Now, the effect is the same so it mostly doesn’t matter, but there is a side effect of doing it this way which is that, on the device, if you draw a very quick semi-circle, for example, it will end up looking like this: Figure 52. Imperfect lines can be created Re-engineer finger doodling to use HTML5 canvas’s pixel manipulation API and see if that solves this problem. http://beej.us/blog/data/html5s-canvas-2-pixel is a useful reference here (though a few years old now)
  • 127.
    11. Splash screen Tokill the dragon, turn to page 84. To hide in the tunnel, keep reading. LOL that was an interactive fiction reference, because this entire chapter is optional. Why? Well it’s about implementing a splash screen which is a contentious issue in the world of Android apps. (It also involves fiddling with our app’s Java sources which is a bit advanced for this book.) A splash screen is displayed after tapping an app’s launcher icon. They tend to fill the screen and disappear after a delay and / or when the app is ready. Plenty of apps don’t have one. Most big name games have one as those apps can take a while to load. Small utility apps (which is what Japxlate is) seem to mostly not have one. Why is this contentious? Well, the thinking is that they form a barrier to using the app. But they are something to look at when larger apps are loading which could be useful. So make up your own mind (http://cyrilmottier.com/2012/05/03/splash-screens-are-evil-dont- use-them is useful) and skip this chapter if you feel Japxlate doesn’t need a splash screen. If you think it does need one - or just want to see how it generally works - then please read on. Hopefully only the first time while the Web SQL database is created, but our app may take a while to start, and we also want to have some branding. So, we will have a splash screen. PhoneGap exposes splash screen control in the “Splashscreen” API. This needs to be installed as a plugin which can be done like this: you@yours$ japxlate]$ phonegap local plugin add https://git-wip-us.apache.org/repos/asf /cordova-plugin-splashscreen.git From PhoneGap v3.3.0 you can simply type phonegap local plugin add org.apache.cordova.splashscreen Under the hood, this command will do a few things for you: • Add feature name=SplashScreen to res/xml/config.xml • Put SplashScreen.java in newly created /src/org/apache/cordova/splashscreen folder (so this is an example of a plugin that will add to the native Java sources) • Put the plugin in /assets/www/plugins/ • Add references to the plugin in /assets/www/cordova_plugins.js Because of the new Plugman, PhoneGap v3.3.0 instead puts the plugin in PROJECTROOT/plugins
  • 128.
    Splash screen 123 Sonow we’ve got the JavaScript API exposing splash screen actions (show or hide). But this plu- gin has also added some native Java (the “SplashScreen” class). Hold on to your hats folks because we’re going to have to fiddle with the app’s Java source code! Don’t worry though, it’s pretty straightforward. Open up Japxlate.java (which is in /src/com/drappenheimer/japxlate), it will look like this: . . public class Japxlate extends CordovaActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.init(); // Set by lt;content src=index.html / in config.xml super.loadUrl(Config.getStartUrl()); //super.loadUrl(file:///android_asset/www/index.html) } } This is the constructor code that gets the app up and running when the launcher icon is tapped. All we do here is load (indirectly from config.xml) index.html into the current activity - which is a WebView browser. Change it to this: public class Japxlate extends CordovaActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.init(); // Set by lt;content src=index.html / in config.xml super.setIntegerProperty(splashscreen, R.drawable.splash); super.loadUrl(Config.getStartUrl(), 5000); //5 seconds timeout for splash //super.loadUrl(file:///android_asset/www/index.html) } } Before super.loadUrl(), we use super.setIntegerProperty() to set the “splashscreen” prop- erty to R.drawable.splash. “R” is a piece of Java and XML magic which basically picks up all the files and resources in the “res” folder and exposes them as useable properties of the “R” object. R.drawable.splash refers to the appropriate splash.png placed in the /res/drawable folder. splash.png will be our image file for the splash screen. We are about to make that, but first let’s take a peek at the /res folder structure:
  • 129.
    Splash screen 124 Figure53. Contents of /res folder (Eclipse IDE) Opening up these folders we see: Figure 54. Exploded contents of /res folder (NetBeans IDE) The icon.png files are the PhoneGap default launcher icons (we’ll be changing these a bit later). As the name suggests, the drawable folder is where we place any kind of image resource that our app needs. What’s going on here is that, as well as the generic drawable folder, we have four other drawable folders tailored to different screen pixel densities. A high-end Android device will have well over 300 pixels-per-inch. In fact a full-HD phone with a compactish (5” or less) display will probably have over 400! Imagine our launcher icon is fixed at 32 pixels square. This is going to be tiny on a display with 400 pixels-per-inch! The more dense the display pixels are, the larger our launcher icon needs to be to stay the same physical size and to show the same amount of detail. This is true for splash screen images, launcher icons, and any drawable. Back to these folders, we have: • drawable-ldpi - “low dots-per-inch” (default launcher icon = 36x36)
  • 130.
    Splash screen 125 •drawble-mdpi - “medium dots-per-inch” (default launcher icon = 48x48) • drawable-hdpi - “high dots-per-inch” (default launcher icon = 72x72) • drawable-xhdpi - “eXtra high dots-per-inch” (default launcher icon = 96x96) (Gory details at http://developer.android.com/guide/practices/screens_support.html) So for our splash screen we need to put a splash.png of correct size in each of these folders. But just to make sure our code works first we can simply put one splash.png in the “drawable” folder. Our device will suss that there is no specific splash image for its native DPI and simply display the default one in the “drawable” folder. Which obviously might look hideous if the image is too small for the display (or vice versa). Let’s do that first just to get the code working and then we can put in the proper images. OK, so make (or get from the Japxlate sources) a muckabout png in portrait shape of size 320x470. It’s going to be the Japxlate “J” in our signature red. Put the png file in the drawable folder and run the app on your device. You should get the image splashed onto the display for 5 seconds before the app starts. It might look very stretched out and jaggedy. Note that the splash screen will not appear when running the app in desktop Chrome (which is a good thing). .. If you’ve added the status bar to the app as discussed at the end of the First things first: The layout chapter, you’ll still have the status bar when the splash screen is being displayed. Hmmm, but what if 5 seconds is too long? The app might load much quicker than that and we want the splash screen to disappear as soon as the app loads. Well I remember seeing something in config.xml: preference name=auto-hide-splash-screen value=true / So maybe the splash screen is auto hiding as soon as the app is ready, and, by coincidence, our app is taking 5 seconds to load? Well, let’s test that by changing the timeout in Japxlate.java to 20 seconds: . . public class Japxlate extends CordovaActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.init(); // Set by lt;content src=index.html / in config.xml super.setIntegerProperty(splashscreen, R.drawable.splash);
  • 131.
    Splash screen 126 super.loadUrl(Config.getStartUrl(),20000); //super.loadUrl(file:///android_asset/www/index.html) } } Run this and we see that the splash screen does indeed hang around for 20 seconds. The app is not quite that slow and so we need a way to hide the splash screen when the app is ready. We learn from http://docs.phonegap.com/en/3.1.0/cordova_splashscreen_splash- screen.md.html#Splashscreen that: “To dismiss the splash screen once the app receives the deviceready event, call the navigator.splashscreen.hide() method.” Let’s try that. Edit the receivedEvent() method in index.js to look like this: // Update DOM on a Received Event receivedEvent: function(id) { console.log('Received Event: ' + id); if (window.cordova) { //actual app navigator.splashscreen.hide(); } . . As the whole of receivedEvent() is called from both our actual device and debugging in desktop Chrome*, we need to check for window.cordova - ie. we’re on the device - before calling navigator.splashscreen.hide();. *Which does not need to be the case but is just how our simple app has evolved. Run this on your device and you will see that the splash screen disappears when the app is ready, much sooner than 20 seconds! Great! OK, we now need to create the actual splash screen images in the correct sizes. The correct sizes for each image should be: • xlarge (xhdpi): at least 960 × 720 • large (hdpi): at least 640 × 480 • medium (mdpi): at least 470 × 320 • small (ldpi): at least 426 × 320 So go ahead and create (or get from the Japxlate repo) your final “J” images in these sizes and put them in the correct drawables folders.
  • 132.
    Splash screen 127 .. Ifthe image with the correct density for the device is missing, the app seems to find and use the closest matching one. Just one problem remains. Rotate your device into landscape and launch the app. Yuck! Did you see the splash image? It was really stretched out and fat. Figure 55. Splash screen when in landscape orientation Well here’s the correct way to solve this. Really for splash images - and other drawables - you should use a “9 patch” graphic. This is a tricky little format where PNG images have a one-pixel border that isn’t displayed, but contains control pixels telling Android which bits of the image to stretch, shrink or leave unaltered when the device is rotated or if the target device’s aspect ratio is not the same as the drawable file in the app. As you can see already, normal PNGs placed in the drawable folder essentially work, so this tutorial won’t cover 9 patch images any more other than to say that the gory details are here: https://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch “A NinePatchDrawable graphic is a stretchable bitmap image, which Android will automatically resize to accommodate the contents of the View in which you have placed it as the background. An example use of a NinePatch is the backgrounds used by standard Android buttons — buttons must stretch to accommodate strings of various lengths. A NinePatch drawable is a standard PNG image that includes an extra 1-pixel-wide border. It must be saved with the extension .9.png, and saved into the res/drawable/ directory of your project.” and that the Android SDK has a little tool called draw9patch (in ANDROID_SDK_HOME/sdk/tools) to make these images which looks like this:
  • 133.
    Splash screen 128 Figure56. Android SDK’s draw9patch utility The PhoneGap / Cordova documentation for v3.3.0 now has a useful summary section on Icons and Splash Screens.
  • 134.
    12. Launcher icon Nowwe move on to our launcher icon. We are provided with a default one which is like a robot cube thing. Let’s replace this with one more relevant for Japxlate. Let’s have one with our “J”. We simply replace the icon.png files in the various drawable folders. The size of each existing icon.png will tell you the required icon size for that density. http://developer.android.com/design/style/iconography.html is a useful design and technical reference. Note that you might, in order for the icon to update on deploy to the device, need to do a Project ⇒ Clean in Eclipse IDE. The PhoneGap / Cordova documentation for v3.3.0 now has a useful summary section on Icons and Splash Screens.
  • 135.
    13. Submitting toGoogle Play [NOTETOSELF expand on this section] This topic deserves - and has - whole books and tutorials dedicated to it. [NOTETOSELF some links would be nice] We’ll cover it in a whistle-stop fashion. To publish your app on the Play Store, follow these steps: 1. Open PROJECTROOT/AndroidManifest.xml and change android:debuggable=true to android:debuggable=false 2. In Eclipse IDE, right-click on the Project and select Android Tools ⇒ Export Signed Application Package 3. Follow the steps (choosing “create new keystore”) and be sure to save the keystore file and password, and the alias password, in your favourite secure place for future reference 4. The final step creates an .apk file in the location you specify. This is what we will upload to google. 5. You will need to attach an Android Developer account to an already existing vanilla Google account. Do this by logging into Google with your vanilla account first, then signing up as an Android developer at https://play.google.com/apps/publish/signup. 6. You’ll need a credit or debit card handy as there is a one-off registration fee of $25. 7. Upload the .apk file to your shiny new Developer account, set the title, description and screenshots (you also need a 512px x 512px hi-res icon) and Bob’s your uncle!
  • 136.
    14. That’s allfolks! [NOTETOSELF this is going to be a wrapping up chat. talk about the app’s pros and cons, PhoneGap pros and cons. And talk about what they need to do and research going forward with their app development. strongly recommend libraries like Sencha Touch, jQuery Mobile and iScroll especially as doing scrolling from scratch took me more than 50% of total development time which is ridiculous!] TODO useful references