111
User Interface Development with jQuery Colin Clark, Fluid Project Technical Lead, Adaptive Technology Resource Centre Antranig Basman, Software Architect, University Cambridge

fuser interface-development-using-jquery

Embed Size (px)

Citation preview

Page 1: fuser interface-development-using-jquery

User Interface Development with jQuery

Colin Clark, Fluid Project Technical Lead, Adaptive Technology Resource Centre

Antranig Basman, Software Architect, University Cambridge

Page 2: fuser interface-development-using-jquery

Topics We’ll Cover

• What is jQuery?

• JavaScript 101: A refresher course

• The jQuery Way

• Finding things

• Modifying elements

• Attaching Events

• Accessibility

• DOM manipulation

• AJAX

• Quick intro to Fluid Infusion

Page 3: fuser interface-development-using-jquery

Example code and exercises

http://source.fluidproject.org/svn/sandbox/jquery-workshop/trunk

Page 4: fuser interface-development-using-jquery

Things You’ll Need

• Your favourite editor

• Aptana or Eclipse?

• A sane browser with a good debugger

• Firefox + Firebug

• A Servlet container

• Tomcat or Jetty

Page 5: fuser interface-development-using-jquery

What is jQuery?

Page 6: fuser interface-development-using-jquery

jQuery solves real problems...

Page 7: fuser interface-development-using-jquery

What is hard?

Page 8: fuser interface-development-using-jquery

What is hard?• browser inconsistencies and bugs

Page 9: fuser interface-development-using-jquery

What is hard?• browser inconsistencies and bugs

• the depth and complexity of the DOM

Page 10: fuser interface-development-using-jquery

What is hard?• browser inconsistencies and bugs

• the depth and complexity of the DOM

• creating a rich and dynamic UI

Page 11: fuser interface-development-using-jquery

What is hard?• browser inconsistencies and bugs

• the depth and complexity of the DOM

• creating a rich and dynamic UI

• the call and response of asynchronous client-server interaction

Page 12: fuser interface-development-using-jquery

Frameworks can help!• Browser Abstraction

• the depth and complexity of the DOM

• creating a rich and dynamic UI

• the call and response of asynchronous client-server interaction

Page 13: fuser interface-development-using-jquery

Frameworks can help!• Browser Abstraction

• DOM traversal, selection, and manipulation

• creating a rich and dynamic UI

• the call and response of asynchronous client-server interaction

Page 14: fuser interface-development-using-jquery

Frameworks can help!• Browser Abstraction

• DOM traversal, selection, and manipulation

• easy and dynamic event binding

• the call and response of asynchronous client-server interaction

Page 15: fuser interface-development-using-jquery

Frameworks can help!• Browser Abstraction

• DOM traversal, selection, and manipulation

• easy and dynamic event binding

• easy-to-use AJAX functionality

Page 16: fuser interface-development-using-jquery

Find something...

Page 17: fuser interface-development-using-jquery

Find something...

and do something with it

Page 18: fuser interface-development-using-jquery

doing something without a framework

function stripeListElements(listID) { // get the items from the list var myItems = getElementsByTagName("li"); // skip line 0 as it's the header row for(var i = 0; i < myItems.length; i++) { if ((i % 2) === 0) { myItems[i].className = "odd"; } } }

Page 19: fuser interface-development-using-jquery

jQuery("li");

doing something with jQuery

Page 20: fuser interface-development-using-jquery

jQuery("li:even");

doing something with jQuery

Page 21: fuser interface-development-using-jquery

jQuery("li:even").addClass("striped");

doing something with jQuery

Page 22: fuser interface-development-using-jquery

Types of framework

Foundational toolkits vs. application frameworks

Page 23: fuser interface-development-using-jquery

Foundational toolkits

• Totally presentation focused

• DOM manipulation

• Event binding

• Ajax

• eg. jQuery, Prototype

Page 24: fuser interface-development-using-jquery

Widget Libraries• Reusable user interface widgets

• Drag & Drop

• Tabs

• Sliders

• Accordions

• etc.

• eg. jQuery UI, Ext, Scriptaculous

Page 25: fuser interface-development-using-jquery

Application frameworks

• Notifications “something changed here”

• Views to help keep your presentational code clean

• Data binding to sync the display with your model

• eg. Fluid Infusion, Sproutcore, etc.

Page 26: fuser interface-development-using-jquery

jQuery in a Nutshell• Everything you need for:

• Finding things

• Styling things

• Manipulating things

• Attaching events

• Making AJAX Requests

• Pretty low-level: you’ll need more

Page 27: fuser interface-development-using-jquery

The jQuery Way

Page 28: fuser interface-development-using-jquery

jQuery Philosophy

• Unobtrusiveness

• Separation of presentation, structure, logic

• Lightweight and DOM oriented

• Doesn’t do everything, but is very focused

• Functional

Page 29: fuser interface-development-using-jquery

JavaScript 101 (quickly)

Page 30: fuser interface-development-using-jquery

• Everything is an object

• Extremely loose type system

• No classes

• Functions are first class

• Some annoying quirks

JavaScript is Different

Page 31: fuser interface-development-using-jquery

• Define variables with var• No need to declare types

var mango = "yum"; mango = 12345; mango = false;

Defining Variables

Page 32: fuser interface-development-using-jquery

• If you omit var, it will be defined as a global variable. • This is extremely dangerous; JavaScript won't warn you!

rottenTomato = "gross!"; // This is global

Defining Variables

Page 33: fuser interface-development-using-jquery

Truthy and Falsey• JavaScript does a lot of automatic type

coercion

• Shades of true and false

• Helpful when evaluating arguments

• Use with care

Page 34: fuser interface-development-using-jquery

Falsey Values• false

• null

• undefined

• ""

• 0 (zero)

• NaN

• Everything else is truthy. Careful...

-1,"false","0" are all true

Page 35: fuser interface-development-using-jquery

Equal vs. EquivalentComparisons are coercive:

1 == "1" // true

0 == false // true

Non-coercive comparison:

0 === false // false

1 !== "1" // true

1 === Number("1") // true

Page 36: fuser interface-development-using-jquery

• At their core, objects are just maps

• Keys can be any string, values can be anything

• Two different ways to access members:

basketOfFruit.kiwis; // dot notation

basketOfFruit["figs"]; // subscript notation

• You can add new members to any object at any time

Objects Are Loose Containers

Page 37: fuser interface-development-using-jquery

{}

Page 38: fuser interface-development-using-jquery

Objects Are Modifiablevar basketOfFruit = {

pears: “bartlett”,

oranges: “mandarin”

};

// New property

basketOfFruit.apples = "macintosh";

// New method

basketOfFruit.eat = function () {

return “tasty”;

};

Page 39: fuser interface-development-using-jquery

No Classes• JavaScript doesn't have any concept of

classes

• Methods are just properties in a container:

• pass them around

• modify them

• delete them

Page 40: fuser interface-development-using-jquery

• Functions are data

• You can assign them

• You can pass them as arguments

• You can return them as results

• Functions can contain member variables

First Class Functions

Page 41: fuser interface-development-using-jquery

Let’s Skip the Theory

1. Functions are real objects.

2. Functions remember the definition of nested variables.

Page 42: fuser interface-development-using-jquery

A Simple Closurevar addNumber = function (a) {

// This function will remember the values of a return function (b) { return a + b; };};

var addOne = addNumber(1); // result is an “add 1” FunctionaddOne(5); // Result is 6addOne(41); // Result is 42

Page 43: fuser interface-development-using-jquery

Getting Started

Page 44: fuser interface-development-using-jquery

A shape for your code

// Your namespace is the only global variable.var namespace = namespace || {};

// A private space, with a helpful alias to jQuery(function ($) {

// To make something available, add it to your namespace.namespace.myFunction = function () {

};

})(jQuery);

Page 45: fuser interface-development-using-jquery

Defining a new thingvar fluid = fluid || {};

(function ($) { // Creator function

fluid.cat = function (name) {// Create your new instancevar that = {};

// Define public variablesthat.name = name;

// Define public methodsthat.meow = function () {

return that.name + ” says meow.”;};

// Return your new instance.return that;

};})(jQuery);

Page 46: fuser interface-development-using-jquery

jQuery === $

Constructor:

$(selectorString | Element | Array | jQuery);

Returns:

A jQuery instance.

Page 47: fuser interface-development-using-jquery

Examples

// Selectorvar allListItems = $(“li”);

// DOM Elementvar theWholeDocument = $(document);

Page 48: fuser interface-development-using-jquery

What’s a jQuery?

• A wrapper for one or many elements

• A real object with useful methods

• A better API than the raw DOM

• Context-oriented and chainable:

$(“li”).addClass(“selected”).attr(“tabindex”, “-1”).text(“Hello!”);

Page 49: fuser interface-development-using-jquery

Basic jQuery Methodsvar allListItems = $(“li”);

// Get the id attributeallListItems.attr(“id”);

// Add a CSS class name.allListItems.addClass(“stripey”);

// Get all the childrenallListItems.children();

// Find items scoped within another jQuery$(“a”, allListItems);

Page 50: fuser interface-development-using-jquery

A Unified API for One or Many

• Most DOM code requires a lot of looping:

• jQuery treats sets the same as single elements:

• Bottom line: no iteration means way less code (and it’s portable)

$("li").attr(“tabindex”, -1);

var myItems = getElementsByTagName("li");

// skip line 0 as it's the header row

for (var i = 0; i < myItems.length; i++) {

myItems[i].tabIndex = -1;

}

Page 51: fuser interface-development-using-jquery

One or many?// Returns the id attribute of the first element.

$(“li”).attr(“id”);

// Sets the tabindex attribute of all elements.

$(“li”).attr(“tabindex”, -1);

// Adds the class name to all elements.

$(“li”).addClass(“highlighted”);

// Returns true if at least one has this class

$(“li”).hasClass(“highlighted”);

Page 52: fuser interface-development-using-jquery

Accessing Members

// Get the element at a specific position, as a jQuery

$(“li”).eq(0);

// Get the element at a specific position,

// as a pure DOM element

$(“li”)[0];

Page 53: fuser interface-development-using-jquery

Selectors

Page 54: fuser interface-development-using-jquery

What’s a Selector?

• Selectors are specified by a string

• A notation for identifying elements in the DOM

• The same thing you use when you’re writing CSS

Page 55: fuser interface-development-using-jquery

Types of Selectors

• Element selectors

• id selectors

• Class name selectors

“div” “span” “ul” “li” “body”

“#flutter-friends” “#friends-error-dialog”

“.invisible” “.flutter-status-panel”

Page 56: fuser interface-development-using-jquery

More Selectors

• Descendent selectors:

• Child selectors:

• Pseudo selectors:

“.flutter-status-panel textarea” ancestor descendent

“#friends>li>img” ancestor child child

“:first” “:even” “:hidden” “:contains(‘John Resig’)

“:not(#flutter-friends-template)”

Page 57: fuser interface-development-using-jquery

Doing Stuff

Page 58: fuser interface-development-using-jquery

Manipulating Attributes

var friends = $(“#friends li”);

// Get the id attributefriends.attr(“id”);

// Set the id attributefriends.attr(“id”, “123456789”);

// attr() also provides normalizationfriends.attr(“tabindex”, -1);

Page 59: fuser interface-development-using-jquery

Manipulating Classes

var friends = $(“#friends li”);

// Add a class namefriends.addClass(“flutter-hidden”);

// Remove a class namefriends.removeClass(“flutter-hidden”);

// Toggle a class name on or offfriends.toggleClass(“flutter-hidden”);

Page 60: fuser interface-development-using-jquery

Directly Manipulating Styles

var friends = $(“#friends li”);

// Get the element’s computed border-color stylefriends.css(“border-color”);

// Set the element’s stylefriends.css(“border-color”, “red”);friends.css(“border”, “5px”);

// Get and set height and widthsettingsPanel.height();settingsPanel.width(400);

Page 61: fuser interface-development-using-jquery

Is the document ready?

• HTML gets parsed by the browser linearly

• Head first, then body, etc.

• So all your <head> scripts will execute immediately

• Need to know as soon as the document is ready

$(document).ready(function () { // This is the earliest point at which // the document is ready.});

$(“li”).length === 0

Page 62: fuser interface-development-using-jquery

Exercise 1

Page 63: fuser interface-development-using-jquery

Finding Things• Using FindingThings.html, find the following things:

• The friends list

• All list items in every list on the page

• The list items inside the friends list

• Everything with the class fl-centered

• The first form element on the page

• The last item in the friends list

• The label for the username text field

• Give each thing a background colour

Page 64: fuser interface-development-using-jquery

Events: Finding Things

Page 65: fuser interface-development-using-jquery

Types of Browser Events

• Mouse

• click() when the mouse button is clicked

• mouseover() when the cursor is over an element

• Keyboard events:

• keydown() as soon as a key is pressed down

• keyup() when the key is released

• keypress() can be buggy and inconsistent

• Other

• focus() when an element is clicked or focused with the keyboard

• blur() when focus leaves the event

Page 66: fuser interface-development-using-jquery

jQuery and Events

• Events vary wildly across browsers

• Netscape vs. IE vs. W3C: they’re all different

• jQuery normalizes all the standard browser events

• Also lets you define your own custom events

Page 67: fuser interface-development-using-jquery

How events work

• Event Bubbling:

• Browser detects an event

• Starts at the immediate target element

• If a handler is not found, the parent is then checked

• And onwards up the tree

• If a handler is present, it is invoked

• Handler can stop bubbling; otherwise, the event propagates up the tree as above

Page 68: fuser interface-development-using-jquery

Binding Events

// The generic way$(“li”).bind(“click”, function (event) {alert(“You clicked me!”);

});

// Event binding shortcut$(“li”).click(function (event) {alert(“You clicked me!”);

});

Page 69: fuser interface-development-using-jquery

Event Handlers• The event object provides more information

about the event that occurred

• this points to the element on which the event listener was bound. Be careful!

• Event handlers always deal with pure elements, not jQuery instances (this and event.target)

var friends = $(“#friends”);friends.click(function (event) { showTweetsForFriend(this); // this === friends[0];});

Page 70: fuser interface-development-using-jquery

The Event Object

{altKey: boolean,ctrlKey: boolean,metaKey: boolean,shiftKey: boolean, // Were these modifier keys depressed?keyCode: Number, // The numeric keycode for key eventswhich: Number, // Keycode or mouse button codepageX: Number, // Horizontal coordinate relative to pagepageY: Number, // Vertical coordinate relative to pagerelatedTarget: Element, // Element left or enteredscreenX: Number, // Horizontal coordinate relative to screenscreenY: Number, // Vertical coordinate relative to screentarget: Element, // The element for which the event was triggeredtype: String // The type of event that occurred (eg. “click”)

}

Page 71: fuser interface-development-using-jquery

Default Actions

• The browser provides a default action for many elements, for example:

• When links are clicked, a new page loads

• When an arrow key is pressed, the browser scrolls

• When Enter is pressed in a form, it submits

• You can prevent it if you want to handle the behaviour yourself:

$(“a”).bind(“click”, function (event) { event.preventDefault();});

Page 72: fuser interface-development-using-jquery

Stopping Propagation• By default, events will propagate up the tree after your

handler runs

• To prevent propagation:

• To swallow propagation and the default action:

$(“a”).click(function (event) { event.stopPropagation();});$(“a”).each(function idx, item) { item.click(function () { item.doSomething(); };};

$(“a”).click(function (event) { return false;});

Page 73: fuser interface-development-using-jquery

Removing Events

// Remove all event listeners.$(“li”).unbind();

// Remove all click event listeners.$(“li”).unbind(“click”);

// Remove a specific listener.var myListener = function (event) {...};$(“li”).bind(myListener);$(“li”).unbind(myListener);

Page 74: fuser interface-development-using-jquery

One-off Events

// This event will only ever fire once.$(“li”).one(function (event) { alert(“You’ll only see me once.”);});

// A more awkward, verbose version:var fireOnce = function (event) { ... };$(“li”).bind(“click”, function (event) { fireOnce(); $(“li”).unbind(fireOnce);});

Page 75: fuser interface-development-using-jquery

Exercise 2: Events

Page 76: fuser interface-development-using-jquery

Binding Events

• Using BindingEvents.html:

• Bind click handlers to each of the friend <li> elements.

• Your click handler should invoke the selectFriend() function with the friend that was clicked.

• The function should use jQuery to adjust the CSS classes of the friend elements so that just the clicked has the flutter-active style

Page 77: fuser interface-development-using-jquery

DOM Manipulation

Page 78: fuser interface-development-using-jquery

Adding things to the DOM

• The traditional DOM API provides methods for creating new elements and adding them to existing elements

• Can also be quite slow

• IE implemented the now ad-hoc standard innerHTML, which was faster

• jQuery provides a great API for DOM manipulation, as well as cross-browser manipulation

• Ultimately, it still uses the DOM APIs underneath: still slow

Page 79: fuser interface-development-using-jquery

More DOM Manipulation

// Create a new element.var myList = $(“<ul></ul>”);

// Appending elements to the end of a container.var otherListItems = $(“li”);myList.append(otherListItems);

// Same result.otherListItems.appendTo(myList);

// Remove an element from the DOM entirely.// Conveniently, this returns the item you just removed$(“#flutter-friend-template).remove();

// Remove all children from a container.myList.empty();

Page 80: fuser interface-development-using-jquery

More manipulation: copying

// Clone an element$(“#flutter-friend-template”).clone();

// Clone an element, along with all its event handlers$(“#flutter-friend-template”).clone(true);

Page 81: fuser interface-development-using-jquery

Getting/Setting Element Values

// Get a value from a form element.$(“#status”).val();

// Set a value on a form element.$(“#status”).val(“Giving a presentation a Jasig.”);

// Getting the text of an element.$(“#status”).text();

// Setting the text of an element.$(“#status”).text(“John Resig”);

Page 82: fuser interface-development-using-jquery

DOM Manipulation Advice

• Try to use CSS instead of DOM manipulation where possible (e.g. hiding/showing elements, etc.)

• DOM manipulation can be very costly

• jQuery’s API is great, but it isn’t magic

• Avoid building up elements one at a time

• Injecting whole blocks of HTML at once:

myContainer.html(“<ul><li>Colin</li><li>Antranig</li><li>Jess</li></ul>”);

Page 83: fuser interface-development-using-jquery

Exercise 3: Manipulation

Page 84: fuser interface-development-using-jquery

Manipulating the DOM

• Using domManipulation.html:

• Bind a key handler to the entry field

• The key handler should i) fetch the field text, ii) clone a template node for a new Twitter item, iii) fill in the node text with the field text, iv) add the template to the twitter list, v) make it visible, vi) clear the entry field

Page 85: fuser interface-development-using-jquery

Accessibility

Page 86: fuser interface-development-using-jquery

DHTML: A New Can of Worms

• The shift from documents to applications

• Familiar a11y techniques aren’t enough

• Most DHTML is completely inaccessible

• New techniques are still being figured out

Page 87: fuser interface-development-using-jquery

Assistive Technologies• Present and control the user interface

in different ways

• Screen readers

• Screen magnifiers

• On-screen keyboards

• Use built-in operating system APIs to understand the user interface

Page 88: fuser interface-development-using-jquery

The Problem• Custom widgets often look, but don’t act,

like their counterparts on the desktop

• HTML provides only simple semantics

• Not enough information for ATs

• Dynamic updates require new design strategies to be accessible

Page 89: fuser interface-development-using-jquery

The Solution• Describe user interfaces with ARIA

• Add consistent keyboard controls

• Provide flexible styling and presentation

Page 90: fuser interface-development-using-jquery

Keyboard Accessibility

Page 91: fuser interface-development-using-jquery

Keyboard Conventions• Tab key focuses the control or widget

• Arrow keys select an item

• Enter or Spacebar activate an item

• Tab is handled by the browser. For the rest, you need to write code.

Page 92: fuser interface-development-using-jquery

Tabbing and Tabindex• Each focusable item can be reached in

sequence by pressing the Tab key

• Shift-Tab moves backwards

• The tabindex attribute allows you to customize the tab order

• tabindex=”-1” removes element from the tab order: useful for custom handlers

Page 93: fuser interface-development-using-jquery

Tabindex examples<!-- Tab container should be focusable --><ul id=”animalTabs” tabindex=”0”> <!-- Individual Tabs shouldn’t be focusable --> <!-- We’ll focus them with JavaScript instead --><li id=”tab1” tabindex=”-1”>Cats</li><li id=”tab2” tabindex=”-1”>Dogs</li><li id=”tab3” tabindex=”-1”>Alligators</li>

</ul>

Page 94: fuser interface-development-using-jquery

Setting Tabindex with jQuery// Put the friends list in the tab order.jQuery(“#friends”).attr(“tabindex”, 0);

// Remove the individual friends from the tab order.// We’ll focus them programmatically with the arrows.jQuery(“#friends li”).attr(“tabindex”, -1);

Page 95: fuser interface-development-using-jquery

Navigating with the Arrow Keys// Make the tabs selectable with the arrow keys.$(“#friends”).fluid(“selectable”, { selectableSelector: “li”});

Page 96: fuser interface-development-using-jquery

Adding Activation Handlers

// Make each tab activatable with Enter & Spacebar

friends.fluid(“activatable”, function(aFriend) {

alert(“You just selected: “ + aFriend.text());

});

Page 97: fuser interface-development-using-jquery

Supporting Assistive Technology

Page 98: fuser interface-development-using-jquery

Opaque Markup// These are tabs. How would you know?<ul><li>Cats</li><li>Dogs</li><li>Gators</li>

</ul><div><div>Cats meow.</div><div>Dogs bark.</div><div>Gators bite.</div>

</div>

Page 99: fuser interface-development-using-jquery

ARIA• Accessible Rich Internet Applications

• W3C specification in the works

• Fills the semantic gaps in HTML

• Roles, states, and properties

• Live regions

Page 100: fuser interface-development-using-jquery

Roles• Describe widgets not present in HTML 4

• slider, menubar, tab, dialog

• Applied using the role attribute

Page 101: fuser interface-development-using-jquery

States and Properties• Added to elements within the DOM

• Properties describe characteristics:

• draggable, hasPopup, required

• States describe what’s happening:

• busy, disabled, selected, hidden

• Applied using custom aria- attributes

Page 102: fuser interface-development-using-jquery

Using ARIA// Now *these* are Tabs!

<ul id=”animalTabs” role=”tablist” tabindex=”0”> <!-- Individual Tabs shouldn’t be focusable -->

<!-- We’ll focus them with JavaScript instead -->

<li id=”cats” role=”tab” tabindex=”-1”>Cats</li>

<li id=”dogs” role=”tab” tabindex=”-1”>Dogs</li>

<li id=”gators” role=”tab” tabindex=”-1”>Gators</li>

</ul>

<div id=”panels”>

<div role=”tabpanel” labelledby=”cats”>Cats meow.</div>

<div role=”tabpanel” labelledby=”dogs”>Dogs bark.</div>

<div role=”tabpanel” labelledby=”gators”>Gators bite.</div>

</div>

Page 103: fuser interface-development-using-jquery

Setting ARIA with jQueryvar friendsList = $(“#friends”);friendsList.attr(“aria-role”, “tablist”);

var friends = jQuery(“li”, friendsList);friends.each(function(idx, friend) {

jQuery(friend).attr(“aria-role”, “tab”);});friends.eq(0).attr(“aria-selected”, “true”);

var panel = jQuery(“#tweets-panel”);panel.attr(“aria-role”, “tabpanel”);

};

Page 104: fuser interface-development-using-jquery

Exercise 4: Accessibility

Page 105: fuser interface-development-using-jquery

AJAX

Page 106: fuser interface-development-using-jquery

What is AJAX?

• A technique for making HTTP requests from JavaScript without loading the page

• Asynchronous, meaning it doesn’t block the UI

• The X stands for XML, but JSON is often more convenient

• The heart of Web 2.0: enables unprecedented dynamism on the Web

Page 107: fuser interface-development-using-jquery

REST• Just the way the Web works

• Resources are the nouns, referred to by URL

• e.g. http://twitter.com/friends

• Representations define a format for a resource (eg. XML or JSON)

• e.g. http://twitter.com/friends.json

• A small set of verbs:

• GET: gets data from the server

• POST: updates data on the server

• DELETE, PUT

Page 108: fuser interface-development-using-jquery

AJAX with jQuery

$.ajax({url: “http://twitter.com/friends”,type:”GET”,dataType: “json”, // “xml”, “json”, “html”, “script”data: { // Object containing query variables id: 123457},success: function (data) { }, // A callback upon successerror: function () { } // Callback if an error occurs

});

Page 109: fuser interface-development-using-jquery

Exercise 5: AJAX

Page 110: fuser interface-development-using-jquery

AJAX calls to Twitter

• Twitter.js contains a stubbed-out object designed to fetch and send data to our Twitter server

• Implement the following methods, using jQuery’s AJAX functions:

• getFriend()

• getTweets()

• postStatus()

Page 111: fuser interface-development-using-jquery

Questions?