Javascript jQuery

Reviews
Shared by: Larry76
Stats
views:
1083
rating:
2(1)
reviews:
0
posted:
5/21/2008
language:
English
pages:
0
Unobtrusive JavaScript with jQuery Simon Willison XTech , 6th May 2008 How I learned to stop worrying and love JavaScript Unobtrusive JavaScript A set of principles for writing accessible, maintainable JavaScript A library that supports unobtrusive scripting Unobtrusive JavaScript Theory jQuery Practice We will cover • • • • The what and why of unobtrusive JavaScript Why use a library at all? Why pick jQuery? How jQuery handles... • • • • DOM manipulation Event handling Animation Ajax Unobtrusive JavaScript Progressive enhancement Rather than hoping for graceful degradation, PE builds documents for the least capable or differently capable devices first, then moves on to enhance those documents with separate logic for presentation, in ways that don't place an undue burden on baseline devices but which allow a richer experience for those users with modern graphical browser software. Steven Champeon and Nick Finck, 2003 Applied to JavaScript • Build a site that works without JavaScript • Use JavaScript to enhance that site to provide a better user experience: easier to interact with, faster, more fun • Start with Plain Old Semantic HTML • Layer on some CSS (in an external stylesheet) to apply the site’s visual design external script file) to apply the site’s enhanced behaviour • Layer on some JavaScript (in an Surely everyone has JavaScript these days? • There are legitimate reasons to switch it off • Some companies strip JavaScript at the firewall • Some people run the NoScript Firefox extension to protect themselves from common XSS and CSRF vulnerabilities • Many mobile devices ignore JS entirely • Screen readers DO execute JavaScript, but accessibility issues mean that you may not want them to The NoScript extension Unobtrusive examples labels.js • One of the earliest examples of this technique, created by Aaron Boodman (now of Greasemonkey and Google Gears fame) How it works • Once the page has loaded, the JavaScript: • Finds any label elements linked to a text field • Moves their text in to the associated text field • Removes them from the DOM • Sets up the event handlers to remove the descriptive text when the field is focused Django filter lists • Large multi-select boxes aren't much fun • Painful to scroll through • Easy to lose track of what you have selected • Django's admin interface uses unobtrusive JavaScript to improve the usability here http://www.neighbourhoodfixit.com/ Implementing Terms and Conditions Bad Have you read our terms and conditions? Also bad Have you read our terms and conditions? Better Have you read our terms and conditions? Better Have you read our terms and conditions? Best Have you read our terms and conditions? Characteristics of unobtrusive scripts • No in-line event handlers • All code is contained in external .js files • The site remains usable without JavaScript • Existing links and forms are repurposed • JavaScript dependent elements are dynamically added to the page JavaScript for sidenotes • When the page has finished loading... • Find all links with class “sidenote” • When they’re clicked: • Launch a popup window containing the linked page • Don’t navigate to the page With JavaScript window.onload = function() { var links = document.getElementsByTagName('a'); for (var i = 0, link; link = links[i]; i++) { if (link.className == 'sidenote') { link.onclick = function() { var href = this.href; window.open(this.href, 'popup', 'height=500,width=400,toolbar=no'); return false; } } } } With JavaScript window.onload = function() { var links = document.getElementsByTagName('a'); for (var i = 0, link; link = links[i]; i++) { if (link.className == 'sidenote') { link.onclick = function() { var href = this.href; window.open(this.href, 'popup', 'height=500,width=400,toolbar=no'); return false; } } } } With JavaScript window.onload = function() { var links = document.getElementsByTagName('a'); for (var i = 0, link; link = links[i]; i++) { if (link.className == 'sidenote') { link.onclick = function() { var href = this.href; window.open(this.href, 'popup', 'height=500,width=400,toolbar=no'); return false; } } } } With JavaScript window.onload = function() { var links = document.getElementsByTagName('a'); for (var i = 0, link; link = links[i]; i++) { if (link.className == 'sidenote') { link.onclick = function() { var href = this.href; window.open(href, 'popup', 'height=500,width=400,toolbar=no'); return false; } } } } Problems Problems • This only executes when the page has completely loaded, including all images • It over-writes existing load or click handlers • It can’t handle class="sidenote external" • It leaks memory in IE 6 Problems • This only executes when the page has completely loaded, including all images • It over-writes existing load or click handlers • It can’t handle class="sidenote external" • It leaks memory in IE 6 • Solving these problems requires cross- browser workarounds. That’s where libraries come in With jQuery jQuery(document).ready(function() { $('a.sidenote').click(function() { var href = $(this).attr('href'); window.open(href, 'popup', 'height=500,width=400,toolbar=no'); return false; }); }); With jQuery jQuery(document).ready(function() { jQuery('a.sidenote').click(function() { var href = $(this).attr('href'); window.open(href, 'popup', 'height=500,width=400,toolbar=no'); return false; }); }); With jQuery jQuery(document).ready(function() { jQuery('a.sidenote').click(function() { var href = jQuery(this).attr('href'); window.open(href, 'popup', 'height=500,width=400,toolbar=no'); return false; }); }); With jQuery jQuery(document).ready(function() { jQuery('a.sidenote').click(function() { var href = jQuery(this).attr('href'); window.open(href, 'popup', 'height=500,width=400,toolbar=no'); return false; }); }); With jQuery jQuery(function() { jQuery('a.sidenote').click(function() { var href = jQuery(this).attr('href'); window.open(href, 'popup', 'height=500,width=400,toolbar=no'); return false; }); }); With jQuery $(function() { $('a.sidenote').click(function() { var href = $(this).attr('href'); window.open(href, 'popup', 'height=500,width=400,toolbar=no'); return false; }); }); With jQuery jQuery(function($) { $('a.sidenote').click(function() { var href = $(this).attr('href'); window.open(href, 'popup', 'height=500,width=400,toolbar=no'); return false; }); }); Advantages • jQuery(document).ready() executes as soon as the DOM is ready traverse the DOM • $('a.sidenote') uses a CSS selector to • .click(function() { ... }) deals with crossbrowser event handling for us • It also avoids IE memory leaks Why jQuery instead of $X? • • • Unlike Prototype and mooTools... • • • • ... it doesn’t clutter your global namespace it’s succinct YAHOO.util.Dom.getElementsByClassName() ... the learning curve is hours, not days Unlike YUI... Unlike Dojo... jQuery characteristics jQuery characteristics • Minimal namespace impact (one symbol) jQuery characteristics • • Minimal namespace impact (one symbol) Focus on the interaction between JavaScript and HTML jQuery characteristics • • • Minimal namespace impact (one symbol) Focus on the interaction between JavaScript and HTML (Almost) every operation boils down to: • • Find some elements Do things with them jQuery characteristics • • • • Minimal namespace impact (one symbol) Focus on the interaction between JavaScript and HTML (Almost) every operation boils down to: • • Find some elements Do things with them Method chaining for shorter code jQuery characteristics • • • • • Minimal namespace impact (one symbol) Focus on the interaction between JavaScript and HTML (Almost) every operation boils down to: • • Find some elements Do things with them Method chaining for shorter code Extensible with plugins Essential tools Firebug extension for Firefox “Inject jQuery” bookmarklet http://icanhaz.com/xtechjs/ jQuery API docs, inc. visualjquery.com Only one function! • Absolutely everything* starts with a call to the jQuery() function • Since it’s called so often, the $ variable is set up as an alias to jQuery • If you’re also using another library you can revert to the previous $ function with jQuery.noConflict(); * not entirely true CSS selectors jQuery('#nav') jQuery('div#intro h2') jQuery('#nav li.current a') CSS selectors $('#nav') $('div#intro h2') $('#nav li.current a') CSS 2 and 3 selectors a[rel] a[rel="friend"] a[href^="http://"] ul#nav > li li#current ~ li (li siblings that follow #current) li:first-child, li:last-child, li:nth-child(3) Custom jQuery selectors :first, :last, :even, :odd :header :hidden, :visible :even, :odd :input, :text, :password, :radio, :submit... :checked, :selected, :enabled, :disabled div:has(a), div:contains(Hello), div:not(.entry) :animated jQuery collections • $('div.section') returns a jQuery collection object • You can call treat it like an array $('div.section').length = no. of matched elements $('div.section')[0] - the first div DOM element $('div.section')[1] $('div.section')[2] jQuery collections • $('div.section') returns a jQuery collection object • You can call methods on it: $('div.section').size() = no. of matched elements $('div.section').each(function() { console.log(this); }); jQuery collections • $('div.section') returns a jQuery collection object • You can call methods on it: $('div.section').size() = no. of matched elements $('div.section').each(function(i) { console.log("Item " + i + " is ", this); }); jQuery collections • $('div.section') returns a jQuery collection object • You can chain method calls together: $('div.section').addClass('foo').hide(); $('div.section').each(function(i) { console.log("Item " + i + " is ", this); }); The jQuery() function • Overloaded: behaviour depends on the type of the arguments • Grab elements using a selector • “Upgrade” existing DOM nodes • Create a new node from an HTML string • Schedule a function for onDomReady • Usually returns a jQuery collection object jQuery methods • I’ve identified four key types of jQuery method: • Introspectors - return data about the selected nodes • Modifiers - alter the selected nodes in some way • Navigators - traverse the DOM, change the selection • DOM modifiers - move nodes within the DOM Introspectors • $('div:first').attr('title') • $('div:first').html() • $('div:first').text() • $('div:first').css('color') • $('div:first').is('.entry') Modifiers • $('div:first').attr('title', 'The first div') • $('div:first').html('New content') • $('div:first').text('New text content') • $('div:first').css('color', 'red') Bulk modifiers • $('a:first').attr({ 'title': 'First link on the page', 'href': 'http://2008.xtech.org/' }); $('a:first').css({ 'color': 'red', 'backgroundColor': 'blue' }); • Notice a pattern? • $(selector).attr(name) gets • $(selector).css(name) gets • $(selector).attr(name, value) sets • $(selector).css(name, value) sets • $(selector).attr({ object }) sets in bulk • $(selector).css({ object }) sets in bulk Style modifiers • • • • • $(selector).css(...) $(selector).addClass(class) $(selector).removeClass(class) $(selector).hasClass(class) $(selector).toggleClass(class) Dimensions • $(selector).height() • $(selector).height(200) • $(selector).width() • $(selector).width(200) • var offset = $(selector).offset() • offset.top, offset.left Navigators - finding • $('h1').add('h2') • $('div:first').find('a') • $('a:first').children() • $('a:first').children('em') • $('a').parent() • $('a:first').parents() • $('a:first').siblings() • $('h3').next() • $('h3:first').nextAll() • $('h3').prev() • $('h3').prevAll() • $('a:first').contents() Navigators - filtering • • • $('div').eq(1) // gets second $('div').filter('.entry') $('div').filter(function(i) { return this.className == 'foo' } • • • $('div').not('.entry') $('div').slice(1, 3) // 2nd,3rd $('div').slice(-1) // last DOM modifiers • • • • • • els.append(content) content.appendTo(els) els.prepend(content) content.prependTo(els) els.after(content) els.before(content) • • • • • • content.insertAfter(els) content.insertBefore(els) els.wrapAll('
) els.wrapInner('
) els.empty() els.remove() DOM construction • var p = $('

').addClass('bar'); • p.text('This is some text').css('color', 'red'); • p.appendTo(document.body); jQuery and Microformats Favourite restaurant list With JavaScript enabled jQuery and Microformats

  • Riddle & Finns

    12b Meeting House Lane

    Brighton, UK

    BN1 1HB

    Telephone: +44 (0)1273 323 008

    Lat/Lon: 50.822563, -0.140457

  • ... Creating the map jQuery(function($) { // First create a div to host the map var themap = $('
    ').css({ 'width': '90%', 'height': '400px' }).insertBefore('ul.restaurants'); // Now initialise the map var mapstraction = new Mapstraction('themap','google'); mapstraction.addControls({ zoom: 'large', map_type: true }); Creating the map jQuery(function($) { // First create a div to host the map var themap = $('
    ').css({ 'width': '90%', 'height': '400px' }).insertBefore('ul.restaurants'); // Now initialise the map var mapstraction = new Mapstraction('themap','google'); mapstraction.addControls({ zoom: 'large', map_type: true }); Displaying the map // Show map centred on Brighton mapstraction.setCenterAndZoom( new LatLonPoint(50.8242, -0.14008), 15 // Zoom level ); Extracting the microformats $('.vcard').each(function() { var hcard = $(this); var latitude = hcard.find('.geo .latitude').text(); var longitude = hcard.find('.geo .longitude').text(); var marker = new Marker(new LatLonPoint(latitude, longitude)); marker.setInfoBubble( '
    ' + hcard.html() + '
    ' ); mapstraction.addMarker(marker); }); Extracting the microformats $('.vcard').each(function() { var hcard = $(this); var latitude = hcard.find('.geo .latitude').text(); var longitude = hcard.find('.geo .longitude').text(); var marker = new Marker(new LatLonPoint(latitude, longitude)); marker.setInfoBubble( '
    ' + hcard.html() + '
    ' ); mapstraction.addMarker(marker); }); Events $('a:first').bind('click', function() { $(this).css('backgroundColor' ,'red'); return false; }); Events $('a:first').click(function() { $(this).css('backgroundColor' ,'red'); return false; }); Event objects $('a:first').click(function(ev) { $(this).css('backgroundColor' ,'red'); ev.preventDefault(); }); Triggering events $('a:first').trigger('click'); Triggering events $('a:first').click(); Events • • • • • • blur() change() click() dblclick() error() focus() • • • • • • keydown() keypress() keyup() load() mousedown() mouseover() • • • • • • mouseup() resize() scroll() select() submit() unload() labels.js with jQuery ... ... labels.js with jQuery jQuery(function($) { $('label.inputHint').each(function() { var label = $(this); var input = $('#' + label.attr('for')); var initial = label.hide().text().replace(':', ''); input.focus(function() { input.css('color', '#000'); if (input.val() == initial) { input.val(''); } }).blur(function() { if (input.val() == '') { input.val(initial).css('color', '#aaa'); } }).css('color', '#aaa').val(initial); }); }); labels.js with jQuery jQuery(function($) { $('label.inputHint').each(function() { var label = $(this); var input = $('#' + label.attr('for')); var initial = label.hide().text().replace(':', ''); input.focus(function() { input.css('color', '#000'); if (input.val() == initial) { input.val(''); } }).blur(function() { if (input.val() == '') { input.val(initial).css('color', '#aaa'); } }).css('color', '#aaa').val(initial); }); }); labels.js with jQuery jQuery(function($) { $('label.inputHint').each(function() { var label = $(this); var input = $('#' + label.attr('for')); var initial = label.hide().text().replace(':', ''); input.focus(function() { input.css('color', '#000'); if (input.val() == initial) { input.val(''); } }).blur(function() { if (input.val() == '') { input.val(initial).css('color', '#aaa'); } }).css('color', '#aaa').val(initial); }); }); labels.js with jQuery jQuery(function($) { $('label.inputHint').each(function() { var label = $(this); var input = $('#' + label.attr('for')); var initial = label.hide().text().replace(':', ''); input.focus(function() { input.css('color', '#000'); if (input.val() == initial) { input.val(''); } }).blur(function() { if (input.val() == '') { input.val(initial).css('color', '#aaa'); } }).css('color', '#aaa').val(initial); }); }); labels.js with jQuery jQuery(function($) { $('label.inputHint').each(function() { var label = $(this); var input = $('#' + label.attr('for')); var initial = label.hide().text().replace(':', ''); input.focus(function() { input.css('color', '#000'); if (input.val() == initial) { input.val(''); } }).blur(function() { if (input.val() == '') { input.val(initial).css('color', '#aaa'); } }).css('color', '#aaa').val(initial); }); }); Advanced chaining • Modified chains can be reverted using end() $('div.entry').css('border', '1px solid black). find('a').css('color', 'red').end(). find('p').addClass('p-inside-entry').end(); Inline form help With JavaScript enabled Form help HTML

    We promise not to spam you!

    Form help JavaScript jQuery(function($) { // Set up contextual help... var contextualHelp = $('
    '); contextualHelp.hide().appendTo(document.body); Form help JavaScript // helpArrow is a div containing the ]- thing var helpArrow = $('
    ').css({ 'position': 'absolute', 'left': '450px', 'top': '0px', // This changes 'height': '22px', // This changes 'width': '20px' }).hide().appendTo(document.body); Form help JavaScript var helpBrace = $('
    ').css({ 'width': '5px', 'height': '100%', 'border-right': '2px solid #ccc', 'border-bottom': '2px solid #ccc', 'border-top': '2px solid #ccc' }).appendTo(helpArrow); Form help JavaScript var helpBrace = $('
    ').css({ 'width': '5px', 'height': '100%', 'border-right': '2px solid #ccc', 'border-bottom': '2px solid #ccc', 'border-top': '2px solid #ccc' }).appendTo(helpArrow); Form help JavaScript var helpBar = $('
    ').css({ 'width': '15px', 'height': '0px', 'border-top': '2px solid #ccc', 'position': 'absolute', 'top': '50%', 'left': '5px' }).appendTo(helpArrow); Form help JavaScript function showHelp(helpWrapper, helpHtml) { // Display contextual help next to helpWrapper div var top = $(helpWrapper).offset().top; helpArrow.css('top', top + 'px'); helpArrow.height($(helpWrapper).height()).show(); contextualHelp.css('top', top + 'px').show().html(helpHtml); } Form help JavaScript function showHelpForField(field) { var helpWrapper = input.parents('div.helpWrapper'); var helpHtml = helpWrapper.find('p.helpText').html(); showHelp(helpWrapper, helpHtml); } Form help JavaScript $('div.helpWrapper').find(':input').focus(function() { showHelpForField(this); }).end().find('p.helpText').hide(); Advanced Events $('a:first').unbind('click'); $('a:first').unbind(); $('a:first').one('click', function() { // executes the first time the link is clicked } $('a:first').toggle(func1, func2); $('a:first').hover(func1, func2); Custom events function updateInbox(event, mail) { alert('New e-mail: ' + mail); } $(window).bind('mail-recieved', updateInbox) $(window).bind('mail-recieved', soundPing) $(window).trigger('mail-recieved', [mail]) Ajax • • Simple: • • • • • • jQuery('div#news').load('/news.html'); jQuery.ajax(options) - low level control jQuery.get(url, [data], [callback]) jQuery.post(url, [data], [callback], [type]) jQuery.getJSON(url, [data], [callback]) jQuery.getScript(url, [data], [callback]) Complex: Ajax global events • .ajaxComplete(function() { }) • .ajaxError(function() { }) • .ajaxSend(function() { }) • .ajaxStart(function() { }) • .ajaxStop(function() { }) • .ajaxSuccess(function() { }) Ajax sidenote $('a.sidenote').one('click', function() { $('

    ').load(this.href).appendTo(document.body); // Make the link stop being a link $(this).replaceWith($(this).contents()); return false; }); Loading... var loading = $( 'loading' ).appendTo(document.body).hide() jQuery(window).ajaxStart(function() { loading.show() }); jQuery(window).ajaxStop(function() { loading.hide() }); jQuery(xml) var signup = $('div#en_sidebar div#signup.rhsbox'); var news_box = $('

    '); news_box.html(signup.html()); news_box.find('div.box').empty(); var ul = $('
      '); var feed_url = jQuery('link[type=application/atom+xml]').attr('href'); jQuery.get(feed_url, function(xml) { var feed = jQuery(xml); feed.find('feed entry').each(function() { var title = $(this).find('title').text(); var link = $(this).find('link').attr('href'); var li = $('
    • ' + title + '
    • '); li.appendTo(ul); }); }); ul.css('text-align', 'left').appendTo(news_box.find('div.box')); news_box.insertBefore(signup); Paste the above in to Firebug on any 2008.xtech.org page Animation • jQuery has built in effects: $('h1').hide('slow'); $('h1').slideDown('fast'); $('h1').fadeOut(2000); • Chaining automatically queues the effects: $('h1').fadeOut(1000).slideDown() You can roll your own $("#block").animate({ width: "+=60px", opacity: 0.4, fontSize: "3em", borderWidth: "10px" }, 1500); A login form that shakes its head The shake animation function shake(el, callback) { el.css({'position': 'relative'}); el.animate( {left: '-10px'}, 100 ).animate( {left: '+10px'}, 100 ).animate( {left: '-10px'}, 100 ).animate( {left: '+10px'}, 100 ).animate( {left: '0px'}, 100, callback ); }; The PHP $username = isset($_POST['username']) ? $_POST['username'] : ''; $password = isset($_POST['password']) ? $_POST['password'] : ''; $msg = ''; if ($_POST) { if ($username == 'simon' && $password == 'xtech') { if (is_xhr()) { json_response(array('ok' => true, 'redirect' => 'loggedin.php')); } else { header('Location: loggedin.php'); } exit; } if (is_xhr()) { json_response(array('ok' => false)); } $msg = '

      Incorrect username or password.

      '; } PHP utility functions function is_xhr() { return (isset($_SERVER["HTTP_X_REQUESTED_WITH"]) && $_SERVER["HTTP_X_REQUESTED_WITH"] == 'XMLHttpRequest'); } function json_response($obj) { header('Content-Type: application/json'); print json_encode($obj); exit; } The JavaScript jQuery(function($) { $('#username').focus(); $('form').submit(function() { var form = $(this); var url = form.attr('action'); var data = form.serialize(); jQuery.post(url, data, function(json) { if (json.ok) { window.location = json.redirect; } else { ... The JavaScript else { $('#password').val('').focus(); shake(form, function() { if (!form.find('p.error').length) { $('

      Incorrect username or ' + 'password.

      ').prependTo(form). hide().slideDown(); } }); } }, 'json'); // jQuery.post(url, data, callback, type); return false; }); }); shake() as a plugin jQuery.fn.shake = function(callback) { this.css({'position': 'relative'}); return this.animate( {left: '-10px'}, 100 ).animate( {left: '+10px'}, 100 ).animate( {left: '-10px'}, 100 ).animate( {left: '+10px'}, 100 ).animate( {left: '0px'}, 100, callback ); }; $('form').shake(); $('form').shake(function() { alert('shaken!') }); jQuery Plugins Plugins • jQuery is extensible through plugins, which • Form: better form manipulation • UI: drag and drop and widgets • $('img[@src$=.png]').ifixpng() • ... many dozens more can add new methods to the jQuery object Logging the chain jQuery.fn.log = function(message) { if (message) { console.log(message, this); } else { console.log(this); } return this; }; jQuery.fn.hideLinks = function() { this.find('a[href]').hide(); return this; } $('p').hideLinks(); jQuery.fn.hideLinks = function() { this.find('a[href]').hide(); return this; } $('p').hideLinks(); jQuery.fn.hideLinks = function() { return this.find('a[href]').hide().end(); } $('p').hideLinks(); Extending the selector engine jQuery.expr[':'].second = function(a,i){return i==1;} $('div:second') - the second div on the page jQuery data() • Attaching data directly to DOM nodes can create circular references and cause memory leaks attaching information • jQuery provides a data() method for safely • $('div:first').data('key', 'value'); • var value = $('div:first').data('key'); jQuery utilities • • • • • jQuery.each(object, callback) jQuery.extend(target, object) jQuery.grep(array, callback) jQuery.map(array, callback) jQuery.inArray(value, array) • • • • jQuery.unique(array) jQuery.makeArray(obj) jQuery.isFunction(obj) jQuery.trim(string) Further reading • http://jquery.com/ • http://docs.jquery.com/ • http://visualjquery.com/ - API reference • http://simonwillison.net/tags/jquery/ • http://simonwillison.net/2007/Aug/15/jquery/ • http://24ways.org/2007/unobtrusivelymapping-microformats-with-jquery

Related docs
JavaScript _ jQuery
Views: 4  |  Downloads: 1
jquery doc
Views: 322  |  Downloads: 67
jQuery Introduction
Views: 639  |  Downloads: 45
An Introduction to jQuery
Views: 186  |  Downloads: 31
jQuery Selectors
Views: 1476  |  Downloads: 71
jquery
Views: 125  |  Downloads: 15
jQuery and Drupal
Views: 6  |  Downloads: 5
Introduction To JQuery
Views: 85  |  Downloads: 30
jQuery Workshop
Views: 103  |  Downloads: 10
jQuery Slides
Views: 136  |  Downloads: 17
jQuery Cheat Sheet
Views: 52  |  Downloads: 7
premium docs
Other docs by Larry76
Africa from A to Z
Views: 25546  |  Downloads: 92
modular css
Views: 803  |  Downloads: 37
powerpoint templates
Views: 1370  |  Downloads: 161
reasons to love a woman
Views: 1160  |  Downloads: 67
the bahamas
Views: 553  |  Downloads: 12
strange food pictures
Views: 4034  |  Downloads: 20
great demo
Views: 491  |  Downloads: 7
marketing plan[1]
Views: 1330  |  Downloads: 107
fingerart images
Views: 291  |  Downloads: 4
barcamp
Views: 234  |  Downloads: 0
beauty and the geek
Views: 288  |  Downloads: 6
ui case studies
Views: 237  |  Downloads: 19
thailand pictures
Views: 364  |  Downloads: 16
Pictures of holland
Views: 660  |  Downloads: 24