1/** 2 * LocalBitcoins.com site-wide Javascript 3 */ 4 5// public utility functions 6localBitcoins = {}; 7 8(function($) { 9 10 "use strict"; 11 12 if (typeof(window.console) === 'undefined') { 13 console = {}; 14 console.log = console.warn = console.error = function(a) {}; 15 } 16 17 // http://stackoverflow.com/questions/3974827/detecting-touch-screen-devices-with-javascript 18 var hasTouch = 'ontouchstart' in document.documentElement; 19 window.hasTouch = hasTouch; 20 21 var isNarrow = window.screen.availWidth < 800; 22 23 // Export currently active language 24 var lang = $("html").attr("lang"); 25 window.lang = lang; 26 27 // Helper for determining max element height of selected elements 28 $.fn.max = function(selector) { 29 return Math.max.apply(null, this.map(function(index, el) { 30 return selector.apply(el); 31 }).get()); 32 }; 33 34 function deleteCookie(name) { 35 document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:01 GMT;'; 36 }; 37 38 // http://stackoverflow.com/questions/10730362/javascript-get-cookie-by-name 39 function readCookie(name) { 40 var nameEQ = name + "="; 41 var ca = document.cookie.split(';'); 42 for (var i = 0; i < ca.length; i++) { 43 var c = ca[i]; 44 while (c.charAt(0) === ' ') { 45 c = c.substring(1, c.length); 46 } 47 if (c.indexOf(nameEQ) === 0) { 48 return c.substring(nameEQ.length, c.length); 49 } 50 } 51 return null; 52 } 53 54 // Make sure cookie value is uri 55 function decodeCookieValue(val) { 56 if(val.substring("%") >= 0) { 57 return window.decodeURIComponent(val); 58 } 59 } 60 window.decodeCookieValue = decodeCookieValue; 61 62 // http://stackoverflow.com/a/13164238/315168 63 function writeCookie(key, value, days) { 64 var date = new Date(); 65 // Get unix milliseconds at current time plus number of days 66 date.setTime(+date + (days * 86400000)); //24 * 60 * 60 * 1000 67 window.document.cookie = key + "=" + '"' + value + '"' + "; expires=" + date.toGMTString() + "; path=/"; 68 return value; 69 } 70 window.writeCookie = writeCookie; 71 72 /** Pop-upify some links */ 73 function popuify() { 74 75 //var psinet = getPSINETScore(); 76 //console.log("Psinet:" + psinet); 77 78 79 var passwordIndicatorInitialized = false; 80 81 if (!$.support.cors) { 82 // IE9 and below 83 // CDN'd Bootstrap does not allow AJAX loading without setting CORS headers 84 // http://stackoverflow.com/questions/10232017/ie9-jquery-ajax-with-cors-returns-access-is-denied 85 return; 86 } 87 88 // Don't do modals on mobile 89 if (hasTouch && isNarrow) { 90 return; 91 } 92 } 93 94 // attach a very simple password strength checker to $selector 95 function passwordStrength($selector) { 96 var $response = $("<span></span>"); 97 $response.addClass("pwstr"); 98 $selector.after($response); 99 $selector.keyup(function() { 100 var $el = $(this); 101 var len = $el.val().length; 102 if (len < 8) { 103 $response.removeClass("pwstr-medium pwstr-good pwstr-better"); 104 $response.addClass("pwstr-weak");
105 $response.html("Very weak"); 106 } else if (len < 16) { 107 $response.removeClass("pwstr-weak pwstr-good pwstr-better"); 108 $response.addClass("pwstr-medium"); 109 $response.html("Medium"); 110 } else if (len < 25) { 111 $response.removeClass("pwstr-medium pwstr-weak pwstr-better"); 112 $response.addClass("pwstr-good"); 113 $response.html("Good"); 114 } else { 115 $response.removeClass("pwstr-medium pwstr-good pwstr-weak"); 116 $response.addClass("pwstr-better"); 117 $response.html("Better"); 118 } 119 }); 120 } 121 window.passwordStrength = passwordStrength; 122 123 // Equalize the height of front page boxes 124 function tuneAdPage() { 125 126 // http://stackoverflow.com/a/9189443/315168 127 $('#ad-tips .well').height(function() { 128 var maxHeight = $('#ad-tips .well').max(function() { 129 return $(this).height(); 130 }); 131 return maxHeight; 132 }); 133 } 134 135 function tuneSplash() { 136 137 function fixSplashScreenHeight() { 138 $('.super-front-page .super-front-page-column').height(function() { 139 var maxHeight = $('.super-front-page .super-front-page-column').max(function() { 140 return $(this).height(); 141 }); 142 return maxHeight; 143 }); 144 } 145 146 fixSplashScreenHeight(); 147 } 148 149 /** Migrated body <script> */ 150 function oldFixes() { 151 152 if (!window.exchange) { 153 throw new Error("JavaScript dynamic options not correctly passed by HTML"); 154 } 155 156 $('ul.nav > li > a[href="' + window.exchange.fullPath.split('?')[0] + '"]').parent().addClass("active"); 157 $(".noscripthidden").show(); 158 $(".noscriptvisible").hide(); 159 } 160 161 /** 162 * In listing rows, make <tr>s clickable 163 * 164 */ 165 function makeListingRowsClickable() { 166 $("tr.clickable").click(function(e) { 167 168 // Only do the magic on left mouse button 169 if (e.which !== 1) { 170 return; 171 } 172 173 var href = $(this).find('.megabutton, .primary-link').attr("href"); 174 175 if (href) { 176 window.location = href; 177 } 178 }); 179 } 180 181 /** 182 * IE8 and FF3.6 do not correctly handle navbar positioning. 183 * 184 * Fixed navbar overlays logged in nav bar because of CSS bugs. 185 * 186 */ 187 function fixIE8NavBar() { 188 189 function checkNavBarHidden() { 190 var bottom = $(".navbar-fixed-top").offset().top + $(".navbar-fixed-top").height(); 191 192 var top = $("#logged_in_bar").offset(); 193 194 if (top) { 195 // No idea why happens. 196 top = top.top; 197 } else { 198 top = 999; 199 } 200 201 if (top < bottom) { 202 $("#logged_in_bar").css("padding-top", "50px"); 203 } 204 } 205 206 if (window.OLD_IE) { 207 $(window).resize(function() { 208 checkNavBarHidden(); 209 }); 210 211 // Fix on load 212 checkNavBarHidden(); 213 } 214 215 } 216 217 /** 218 * Toggle on/off arrow for Twitter Bootstrap collapsibles. 219 * 220 * Multi-collapsible-friendly; supports several collapsibles in the same group, on the same page. 221 */ 222 function animateCollapsibles() { 223 224 $('.collapse').on('show', function() { 225 var $t = $(this); 226 var header = $("a[href='#" + $t.attr("id") + "']"); 227 header.find(".fa-chevron-right").removeClass("fa-chevron-right").addClass("fa-chevron-down"); 228 }).on('hide', function() { 229 var $t = $(this); 230 var header = $("a[href='#" + $t.attr("id") + "']"); 231 header.find(".fa-chevron-down").removeClass("fa-chevron-down").addClass("fa-chevron-right"); 232 }); 233 } 234 235 function handleOnlineStatus() { 236 $(".online-status").tooltip(); 237 } 238 239 // Set special marker class to hide the front page form on IE8 240 function handleFrontPageFormIE8() { 241 if (window.OLD_IE) { 242 $("body").addClass("msie8"); 243 } 244 } 245 246 /** 247 * Handle notifications cross-tab friendly manner with visual and audiable feedback. 248 */ 249 function handleNotifications() { 250 251 if (!$(".notifications_dropdown0").size()) { 252 // Not logged in 253 return; 254 } 255 256 // Set menu initial mute state 257 handleMuteState(); 258 259 // For anonymous users and users without opened trades do not do notifications 260 // Also notifications may be disabled in the site settings. 261 var needNotifications = $(".notification-timestamp").attr("data-need-notifications") == "true"; 262 if(!needNotifications) { 263 return false;
264 } 265 266 // In notifications menu, show mute or unmute 267 function handleMuteState() { 268 if (notifications.getAudible()) { 269 $(".mute-notifications").show(); 270 $(".unmute-notifications").hide(); 271 } else { 272 $(".mute-notifications").hide(); 273 $(".unmute-notifications").show(); 274 } 275 276 $(".mute-notifications").click(function() { 277 notifications.setAudible(false); 278 handleMuteState(); 279 }); 280 $(".unmute-notifications").click(function() { 281 notifications.setAudible(true); 282 handleMuteState(); 283 }); 284 } 285 286 // Simulate that we get changes from the server on 1/3 of polls 287 function fetchData(callback) { 288 289 var url = $(".notifications_dropdown0").attr("data-notification-url"); 290 var timestamp = $(".notification-timestamp").attr("data-timestamp"); 291 292 var buster = Math.random() * 10000; 293 $.ajax({ 294 url: url, 295 data: {"last-timestamp": timestamp, "cache-buster": buster}, 296 success: function(data) { 297 298 // 304 not modified 299 if(!data) { 300 return; 301 } 302 303 data = data.trim(); 304 305 // A valid response, but does not actually 306 // contain any notifications 307 if(!data) { 308 return; 309 } 310 311 var $data = $(data); 312 var silent; 313 314 if($data.find(".notification-menu-entry").length === 0) { 315 // We got response all notifications seen 316 silent = true; 317 } else { 318 // New notifications 319 silent = false; 320 } 321 322 callback(data, silent, false); 323 }, 324 error: function(xhr) { 325 // Got error (usually 403 logged out) from the server, 326 // time to stop polling 327 callback("", false, true); 328 } 329 }); 330 } 331 332 // Simulate changes HTML on the page with new notification list 333 function updatePage(data, source, action) { 334 $(".notifications_dropdown0").html(data); 335 handleMuteState(); 336 } 337 338 function parseAndShowNotifications(oldValue, newValue) { 339 if (!window.exchange.webNotificationsEnabled) { 340 return; 341 } 342 343 // Use difference between old and new values to check what messages are new ones 344 var oldMessages = $(oldValue).find(".unread-msg"); 345 var newMessages = $(newValue).find(".unread-msg"); 346 var notifs = []; 347 for (var i = 0; i < newMessages.length; ++ i) { 348 var found = false; 349 var newMessage = newMessages[i]; 350 for (var j = 0; j < oldMessages.length; ++ j) { 351 var oldMessage = oldMessages[j]; 352 if (oldMessage.firstChild.href == newMessage.firstChild.href && oldMessage.firstChild.innerHTML == newMessage.firstChild.innerHTML) { 353 found = true; 354 break; 355 } 356 } 357 if (!found) { 358 var url = newMessage.firstChild.href; 359 var msg = newMessage.firstChild.innerHTML; 360 361 var notification = new Notification("LocalBitcoins.com", { 362 icon: "/static/img/touch-icon-144.png", 363 tag: url + msg, 364 body: msg 365 }); 366 } 367 } 368 } 369 370 function showExternalNotifications(oldValue, newValue) { 371 if (Notification && Notification.permission === "granted") { 372 parseAndShowNotifications(oldValue, newValue); 373 } else if (Notification && Notification.permission !== "denied") { 374 Notification.requestPermission(function (status) { 375 if (Notification.permission !== status) { 376 Notification.permission = status; 377 } 378 if (status === "granted") { 379 parseAndShowNotifications(oldValue, newValue); 380 } 381 }); 382 } 383 } 384
385 function detectWebNotificationEnabling() { 386 var checkbox = $('#id_enable_web_notifications'); 387 checkbox.on("click", function() { 388 if($(this).is(":checked")) { 389 if (Notification && Notification.permission !== "denied") { 390 Notification.requestPermission(function (status) { 391 if (Notification.permission !== status) { 392 Notification.permission = status; 393 } 394 }); 395 } 396 } 397 }); 398 } 399 400 // Start notification poller 401 notifications.init({ 402 interval: 45 * 1000, 403 fetcher: fetchData, 404 updater: updatePage, 405 showExternalNotifications: showExternalNotifications, 406 debug: false, 407 hasChanged: function(oldValue, newValue) { 408 409 // Assume store notification value is the DOM string dump of the notification HTML 410 411 if (!oldValue) { 412 return false; 413 } 414 // reparse DOM to get rid of all extra, 415 // so that we don't get alarms when the notification menu is source code changes, 416 // but actual DOM matches (whitespaces, settings style attribute, etc.) 417 oldValue = $(oldValue.trim()).find(".notification-menu-entry"); 418 newValue = $(newValue.trim()).find(".notification-menu-entry"); 419 420 // .html() gets the source code only for the first element of jQuery selection, 421 // thus if we have two messages from the same person, 422 // the second notification has same DOM and would not trigger change. 423 // We work around this by converting all menu body notifications to joined HTML string. 424 oldValue = oldValue.map(function() { 425 return this.outerHTML; 426 }).get().join(); 427 newValue = newValue.map(function() { 428 return this.outerHTML; 429 }).get().join(); 430 431 // TODO: Add a separate handler we actually have a notifications in the payload 432 // and e.g. not logged out response 433 return oldValue !== newValue; 434 } 435 }); 436 437 // Pass the latest notification data, as received with the <body> 438 // to the other tabs. 439 var notificationData = $(".notifications_dropdown0").html(); 440 notifications.trigger("notification-received", notificationData, true); 441 442 // When the page is opened, assume we get the notification payload 443 // in the HTML body and send a message that blinking should be stopped. 444 notifications.trigger("notification-noticed"); 445 446 // This only matters when we are on settings page 447 detectWebNotificationEnabling(); 448 449 } 450 451 // Intall JavaScript logic 452 $(document).ready(function() { 453 oldFixes(); 454 popuify(); 455 makeListingRowsClickable(); 456 animateCollapsibles(); 457 handleOnlineStatus(); 458 handleFrontPageFormIE8(); 459 }); 460 461 // Install post-page render logic 462 $(window).load(function() { 463 tuneSplash(); 464 tuneAdPage(); 465 fixIE8NavBar(); 466 handleNotifications(); 467 }); 468 469})(jQuery); 470 471 472// Returns a function, that, as long as it continues to be invoked, will not 473// be triggered. The function will be called after it stops being called for 474// N milliseconds. If `immediate` is passed, trigger the function on the 475// leading edge, instead of the trailing. 476function debounce(func, wait, immediate) { 477 var timeout; 478 return function() { 479 var context = this, args = arguments; 480 var later = function() { 481 timeout = null; 482 if (!immediate) func.apply(context, args); 483 }; 484 var callNow = immediate && !timeout; 485 clearTimeout(timeout); 486 timeout = setTimeout(later, wait); 487 if (callNow) func.apply(context, args); 488 }; 489}; 490 491 492// https://docs.djangoproject.com/en/1.7/ref/contrib/csrf/#ajax 493// using jQuery 494function getCookie(name) { 495 var cookieValue = null; 496 if (document.cookie && document.cookie != '') { 497 var cookies = document.cookie.split(';'); 498 for (var i = 0; i < cookies.length; i++) { 499 var cookie = jQuery.trim(cookies[i]); 500 // Does this cookie string begin with the name we want? 501 if (cookie.substring(0, name.length + 1) == (name + '=')) { 502 cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 503 break; 504 } 505 } 506 } 507 return cookieValue; 508}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.