$(function(){ // Create Temboo namespace window.Temboo || (window.Temboo = function(){ var DEBUG = false; var init = function(){ // // Do...something, someday // // Notify the world $(window).trigger('Temboo.initialized'); } /* Constants */ const ASCII_BACKSPACE = 8; const ASCII_TAB = 9; const ASCII_CARRIAGE_RETURN = 13; const ASCII_SPACE = 32; const ASCII_COMMA = 188; /** * Public-facing methods */ var publicFacing = { /* Constants */ 'ASCII_BACKSPACE': ASCII_BACKSPACE, 'ASCII_TAB': ASCII_TAB, 'ASCII_CARRIAGE_RETURN': ASCII_CARRIAGE_RETURN, 'ASCII_SPACE': ASCII_SPACE, 'ASCII_COMMA': ASCII_COMMA, setEnv:function(env) { DEBUG = env == 'DEVELOPMENT'; }, error:function(){ if(DEBUG && typeof console !== 'undefined'){ var msg = arguments.length > 1 ? arguments[0] + ': ' + arguments[1] : arguments[0]; console.error(msg); } }, log:function(){ if(DEBUG && typeof console !== 'undefined'){ var msg = arguments.length > 1 ? arguments[0] + ': ' + arguments[1] : arguments[0]; //console.log(msg); } }, warn:function(){ if(DEBUG && typeof console !== 'undefined'){ var msg = arguments.length > 1 ? arguments[0] + ': ' + arguments[1] : arguments[0]; console.warn(msg); } }, trackEvent:function(/* polymorphic */) { var evt = ['_trackEvent']; for(var i in arguments) evt.push(arguments[i]); Temboo.log('Temboo.trackEvent', JSON.stringify(evt)); _gaq.push(evt); }, trackPageview:function(uri) { var pageview = ['_trackPageview']; pageview.push(uri); Temboo.log('Temboo.trackPageview', JSON.stringify(pageview)); _gaq.push(pageview); }, /** * For e.g.: the /myaccount/referrals invite pane, sending every address to the validation helper seems a bit * excessive. Here's a quick JS email validator. * * @param allegedAddress String to check. * @return true if address matches the pattern, false if it doesn't. */ quickEmailValidator:function(allegedAddress) { // 1. contains 1 and only 1 "@" // $emailParts = explode('@', $email); var emailParts = allegedAddress.split('@'); if(emailParts.length != 2) { return false; } // email part before @ is not empty if (emailParts[0].length == 0) {return false; } // 2. contains least one "." after the @ // and contains at least two non-empty domain parts var domainParts = emailParts[1].split('.'); for (var i = 0; i < domainParts.length; i++) { if (domainParts[i].length == 0) { return false; } } if (i < 2) { return false; } // 3. is at least 6 characters long if(allegedAddress.length < 6) { return false; } // 4. contains no commas if(allegedAddress.indexOf(',') > -1) { return false; } // 5. contains no spaces if(allegedAddress.indexOf(' ') > -1) { return false; } // hurray, this string is a minimally plausible email address! return true; }, /** * Say we want to gate a feature instantiated entirely in JS. What do we do? Unfortunately, this. * @param name String of name of feature about which we are curious. * @return true if the user has access to the feature, false if it doesn't. */ hiddenFeatureCheck:function(featureName) { var salientVal; $.ajax({ url: '/features/check/' + featureName + '/', async: false, success: function(data) { if (data == 'true') { salientVal = true; } else { salientVal = false; } } }); return salientVal; }, /** * Add an object to the window.Temboo namespace, by name. If the public namespace requested * is already defined an exception is thrown. * * @param name The name by which the object may be accessed, e.g. @ window.Temboo._name_here_ * @param obj The object itself */ register:function(name, obj, callback){ if(typeof this[name] !== 'undefined') throw "Object exists: window.Temboo." + name; this[name] = obj; if(typeof callback === 'function') callback.call(null); this.log('Temboo.register', 'Registered object: Temboo.' + name); $(window).trigger('Temboo.' + name + '.initialized'); }, /** * Vertically center callouts and arrows relative to the element it points to ('.anchor' sibling) * * @param callout The callout element */ // Vertically center callouts and arrows relative to the element it points to ('.anchor' sibling) alignCallout:function(callout) { callout.css('top', (((callout.height() - callout.parent().find('.anchor').height()) / 2)) * -1); callout.find('.head-callout').css('top', ((callout.innerHeight() + callout.height()) / 4) - 2); // half of average of height and height with padding }, /** * Display a brief status notification */ showStatus:function(status, message, options){ var layout, defaults = { anchor:'top', container:'body > .content', displayDuration:8000, fadeDuration:200, applyLayout:true }, options = $.extend({}, defaults, options), $container = $(options['container']), yPos = $container.offset().top; $view = $('
') .attr('id', 'widget-status') .addClass('status-alert ' + status) .append($('').html(message)); // Inject into DOM in order to calculate width $container.append($view); if (options['applyLayout']) { // Position on top edge of the footer, unless that's off-screen // in which case we anchor it to the viewports lower edge if(yPos < $(window).scrollTop()) { layout = { position:'fixed', bottom:'auto', top:0 } } else{ layout = { position:'absolute', bottom:'auto', top:yPos } } layout.width = $view.outerWidth(true) + 'px'; // Apply layout $view.css(layout); } // Fade in, fade out, remove $view .fadeIn(options['fadeDuration']) .delay(options['displayDuration']) .fadeOut(options['fadeDuration'], function(){ $(this).remove(); }); }, /* * Open login callout in header */ showLogin: function() { $navLinks = $('#nav-links'); //$navLinks $('#nav-links').addClass('login-open'); // Restore initial login state restoreInitialLoginState($(this)); }, /** * A function to toggle the UI switch used to select binary units in library.js, plc.js, and activity.js * @param $clicked The switch element to toggle * @param duration The duration for the animation * @param callback A callback function for the animation */ toggleSwitch: function ($clicked, duration, callback) { if (!$clicked.hasClass('sliding')) { $clicked.addClass('sliding'); var $active = $('span.active', $clicked); var width = $active.outerWidth(); var delta = '-=' + width; if ($active.hasClass('right')) { delta = '+=' + width; } var $slider = $('.slider', $clicked); $slider.animate({'left': delta}, duration, 'swing', callback); } }, /** * Sets up the geocoder input field used when editing devices and creating devices. * @param validateOnSuccess A function to be called after a successful entry */ 'autocomplete': function autocomplete(validateOnSuccess) { $("#autocomplete").autocomplete({ "autoFocus": true, "source": function(request, response) { $.post("/device/autocomplete/" + request.term, function(myResponse) { if (myResponse.status === "success") { var myData = JSON.parse(myResponse.data); var result = []; for (var i = 0; i < myData.features.length; i++) { result.push(myData.features[i].place_name); } response(result); } else { // Error, do nothing } }); }, "select": function(e, ui) { $("#autocomplete").attr('data-state', 'validating'); var searchText = ""; if (e.keyCode === ASCII_CARRIAGE_RETURN) { if ($("#autocomplete").val().length === 0) { e.preventDefault(); return false; } searchText = $("#autocomplete").val(); } else { searchText = ui.item.value; } $.post("/device/autocomplete/" + searchText, function(myResponse) { if (myResponse.status == "success") { var myData = JSON.parse(myResponse.data); $("#autocomplete").val(myData.features[0].place_name).autocomplete("close"); $("input#latitude").val(myData.features[0].geometry.coordinates[1]); $("input#longitude").val(myData.features[0].geometry.coordinates[0]); $("#autocomplete").attr("data-state", "validated"); validateOnSuccess(); } else { // Error, do nothing } }); event.preventDefault(); return false; } }); }, /** * Toggle display of location or latitude/longitude fields */ 'toggleLatLon': function toggleLatLon(toggleElement) { if ($('#addressBox input#autocomplete').is(':visible')) { $('input.latlon').show(); $('input.latlon#latitude').focus(); $('#addressBox input#autocomplete').hide(); toggleElement.text('Use street address'); } else { $('input.latlon').hide(); $('#addressBox input#autocomplete').show().focus(); if ($("#addressBox input#autocomplete").val() == '') { $("#addressBox input#autocomplete").removeAttr("data-state"); } toggleElement.text('Use lat/long'); } }, /** * This function is used to validate emails for sensor/diagnostic alerts * @param $currInvite The .curr-invite element to validate */ 'validateAlertEmail': function($currInvite) { var newItem = $('').addClass('invite-address-outer'); var newEntry = $('').addClass('invite-address'); var address = $currInvite.val(); if (address === '') { return; } if ($currInvite.closest('.emails').hasClass('sms')) { // SMS if (address.match(/^\+?[0-9-]+?$/)) { newEntry.addClass('valid'); } else { newEntry.addClass('invalid'); } } else { // Email if (Temboo.quickEmailValidator(address)) { newEntry.addClass('valid'); } else { newEntry.addClass('invalid'); } } var addressText = $('').addClass('address-text'); var newButton = $('').addClass('clear-invite-address icon right'); addressText.html(address); newEntry.append(addressText); newEntry.append(newButton); newItem.append(newEntry); $('.curr-invite', $currInvite.parent()).before(newItem); $('.curr-invite', $currInvite.parent()) .css('width', '1px') .val(''); }, /** * Get the absolute timestamp in account holder's timezone according to the following format: * month day, year time timezone * @param when - in milliseconds * @param timezone - in string (e.g., Pacific/Honolulu) * @param isTime - true if the clock time is wanted in the output * @param showTimezone - false if timezone should not be shown */ 'getAbsoluteTimestamp': function(when, timezone, isTime, showTimezone, longOption) { var date = new Date(parseInt(when)); if (showTimezone == null) { showTimezone = true; // Default to showing timezone } // how the time is represented if (longOption == null) { var options = { hour12: false, hourCycle: 'h24', year: 'numeric', month: 'short', day: 'numeric' }; } else { var options = { hour12: false, hourCycle: 'h24', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }; } if (showTimezone) { options['timeZone'] = timezone ? timezone : 'UTC'; options['timeZoneName'] = 'short'; } var timestamp = isTime ? date.toLocaleTimeString('en-US', options) : date.toLocaleDateString('en-US', options); return timestamp; }, /** * Get the timestamp in one of the following format: * ---------------------- * input: (x milliseconds, 0, false) --> output: z days * minute: (x milliseconds, y milliseconds, true) --> output: z hours "ago" * ---------------------- * @param time1 - time frame in milliseconds * @param time2 - time frame in milliseconds * @param isRelative - if true the relative timestamp will be computed. * * If isRelative = true, time2 should always be the current time. * * when each passes its limit, it converts to the next format. * for relative timestamp, after it reaches 24h * the format is: month day. * after 1 year, the format is: month day, year. */ 'getTimestamp': function(time1, time2, isRelative) { var t1 = parseInt(time1); var t2 = parseInt(time2); var diff = Math.abs(t2 - t1); var timestamp; // Less than a minute (60000 ms) if (diff < 60000) { var second = Math.floor(diff/1000); timestamp = second + ' second' + (second > 1 ? 's' : ''); } // Less than an hour (3600000 ms) else if (diff >= 60000 && diff < 3600000) { var minute = Math.floor(diff/60000); timestamp = minute + ' minute' + (minute > 1 ? 's' : ''); } // Less than a day (86400000 ms) else if (diff >= 3600000 && diff < 86400000) { var hour = Math.floor(diff/3600000); timestamp = hour + ' hour' + (hour > 1 ? 's' : ''); } // Get timestamp for a given millisecond time frame if(!isRelative) { // Less than a week (604800000 ms) if (diff >= 86400000 && diff < 604800000) { var day = Math.floor(diff/86400000); timestamp = day + ' day' + (day > 1 ? 's' : ''); } // Less than a month (2592000000 ms) else if (diff >= 604800000 && diff < 2592000000) { var week = Math.floor(diff/604800000); timestamp = week + ' week' + (week > 1 ? 's' : ''); } // Less than a year (31104000000 ms) else if (diff >= 2592000000 && diff < 31104000000) { var month = Math.floor(diff/2592000000); timestamp = month + ' month' + (month > 1 ? 's' : ''); } // More than a year else if (diff >= 31104000000) { var year = Math.floor(diff/31104000000); timestamp = year + ' year' + (year > 1 ? 's' : ''); } // Get relative timestamp from past to future = now } else { if (diff >= 86400000) { t2 = new Date(time2); t1 = new Date(time1); var month = t1.toLocaleString("en-us", { month: "short" }); // If different years, print target year var isSameYear = (t2.getUTCFullYear() - t1.getUTCFullYear() === 0) ? '' : ', ' + t1.getUTCFullYear(); timestamp = month + ' ' + t1.getUTCDate() + isSameYear; } } return timestamp; }, /** * Capitalize the first character * @param str: string value */ 'capitalize': function(str) { return str.charAt(0).toUpperCase() + str.slice(1); }, /** * Construct an id given a name. * @param nameForId The name to make the id from */ 'constructId': function(nameForId) { var id = nameForId; id = id.replace(/[^a-zA-Z0-9_\s]/g, ''); id = id.replace(/\s+/g, '_'); id = id.toLowerCase(); return id; }, /** * */ 'csvToJSON': function(csv) { // Remove \r var sanitizedCsv = csv.replace(/\r/g, ""); var lines = sanitizedCsv.split("\n"); var result = []; var headers = lines[0].split(","); for (var i = 1; i < lines.length; i++) { var currentline = lines[i].match(/(".*?"|[^",]+)(?=\s*,|\s*$)/g); currentline = currentline || []; var obj = {}; for (var j = 0; j < headers.length; j++) { obj[headers[j]] = currentline[j]; } result.push(obj); } return result; }, /** * Filters all inputs with the given class name with the given regex * @param className - The input element class to filter (should have the preceding dot for jQuery) * @param regex - The regex to filter with */ 'filteredInput': function(className, regex) { $('input' + className).off('cut keyup paste').on('cut keyup paste', function() { var userVal = $(this).val(); var formattedVal = userVal.replace(regex, ''); // Store current cursor positions var originalLength = userVal.length, start = this.selectionStart, end = this.selectionEnd; $(this).val(formattedVal); // Restore current cursor positions if (originalLength == userVal.length) { this.setSelectionRange(start, end); } else { var difference = originalLength - userVal.length; this.setSelectionRange(start - difference, end - difference); } }); }, // Tests whether arr2 contains arr1 'arrayContains': function(arr1, arr2) { for (var i = 0; i < arr1.length; i++) { if ($.inArray(arr1[i], arr2) === -1) { // A value in arr1 is not in arr2, return false return false; } } return true; }, // Find the average of an array 'avg': function(arr) { if (arr.length) { return arr.reduce(function(a, b) {return a + b;}) / arr.length; } return null; }, // Find the average of an array of digital values // Nonzero values are evaluated as 1, zero values are 0 'digitalAvg': function(arr) { if (arr.length) { var binaryArr = []; $.each(arr, function(index, value) { if (value == 0) { binaryArr.push(0); } else { binaryArr.push(1); } }); return Temboo.avg(binaryArr); } return null; }, // Find the standard deviation of an array, given the mean 'stdev': function(arr, mean) { if (arr.length) { var squareDiffs = arr.map(function(value) { return Math.pow((value - mean), 2); }); return Math.sqrt(Temboo.avg(squareDiffs)); } return null; }, // Find median of an array // values: Array 'median': function(values) { values.sort(function(a,b){ return a-b; }); var half = Math.floor(values.length / 2); if (values.length % 2) { return values[half]; } else { return (values[half - 1] + values[half]) / 2.0; } } }; // Do our own initialization init(); // Make it public return publicFacing; }()); // End namespace var restoreInitialLoginState = function(thisObj) { $(thisObj).find('#old-login-lightbox').css('opacity', 1); $(thisObj).find('#old-forgot').css('opacity', 0).hide(); $(thisObj).find('.callout').height('auto'); $(thisObj).find('.callout input#username').focus(); $(thisObj).find('#old-login-lightbox .password-success-message').hide(); }; $('.mapboxgl-ctrl-icon').removeAttr('title'); // // Global behaviors for elements/widgets/whatever that can appear // anywhere across the site. Elements which are more localized // should have their behaviors defined either in view snippets or // in a specific JS file for that area (e.g. myaccount.js) // // Style select.tmb elements $('select.tmb').each(function(){ var sel = $(this); if(sel.attr('placeholder')){ sel.prepend( $('') .attr('class', 'tmb-placeholder') .attr('value', '') .attr('disabled', 'disabled') .attr('selected', 'selected') .text(sel.attr('placeholder')) ); sel.val(''); // Curse you IE8 } }); // Make pre.prettyprint elements text-copyable $('pre.prettyprint').not('.no-copy').copyable(); // Set up the video iframe var $iframe = $('') .attr('height', 600) .attr('width', 960) .attr('frameborder', 0) .attr('webkitAllowFullScreen', true) .attr('mozallowfullscreen', true) .attr('allowFullScreen', true); // Handle clicks on video links, launching in a videobox $('a.video').click(function(event){ event.preventDefault(); event.stopImmediatePropagation(); $iframe.attr('src', $(this).attr('href')).videobox(); }); /* Header login and forgot password callouts */ $navLinks = $('#nav-links'); // Keep login callout open if text entered $('.old-login input', $navLinks).keypress(function() { $navLinks.addClass('login-open'); }); // Track clicks on header links $('.callout-container.documentation .subnav a', $navLinks).click(function(e) { var lang = $(this).attr('href').substr(1); // remove leading / Temboo.trackEvent('Header', 'Docs', lang); }); $('#navigation_iotapps, #navigation_library', $navLinks).click(function(e) { Temboo.trackEvent('Header', e.target.innerHTML); }); // Show/hide login callout var clickOutsideCallout = function(event) { if (!$navLinks.hasClass('login-open') && ($(event.target).closest('.callout-container.login').length > 0 || $(event.target).is('a.login-link') || $(event.target).closest('li.new-device-network-credential.logged-out').length > 0 || $(event.target).closest('li.new-gateway-credential.logged-out').length > 0)) { // If clicking on login link or within callout while callout is not open (persistent), show callout event.preventDefault(); Temboo.showLogin(); } else if ($navLinks.hasClass('login-open') && $(event.target).closest('.callout-container.login .callout').length == 0) { // If clicking outside callout while callout is shown (persistent), hide callout $navLinks.removeClass('login-open'); } }; $(document).off('click', clickOutsideCallout).on('click', clickOutsideCallout); // Login callout submit button $('.old-login').on('click', 'button', function(event) { event.preventDefault(); // Get this login form - distinguish between callout and /login forms var form = $(this).closest('form[name="old-login"]'); $('#username', form).val($('#username', form).val().trim()); $('button', form).fadeTo('fast', 0, function() { $('.spinner', form).delay(300).fadeIn(500); form.unbind('submit'); form.submit(); }); }); $('#old-login input').keypress(function (e) { if (e.which == 13) { $('form#old-login').submit(); return false; //<---- Add this line } }); // Forgot lightbox $(document).on('click', '.old-login a#forgot', function(event) { event.preventDefault(); var calloutReference = $(this).parents('.callout.shadow'); var oldForgotReference = calloutReference.find('#old-forgot'); oldForgotReference.find('.error').remove(); $(calloutReference).find('#forgot_username').val($(calloutReference).find('#username').val()); $(calloutReference).find('#old-login-lightbox').fadeTo(100, 0, 'linear', function() { $(calloutReference).animate({'height': $(oldForgotReference).outerHeight()}, 100, 'linear', function() { $(oldForgotReference).show().fadeTo(100, 1, 'linear'); $(oldForgotReference).find('#forgot_username').focus(); }) }); }); // Forgot submit $(document).on('click', '.callout #old-forgot button#forgot_submit', function(e) { e.preventDefault(); var $forgotContainer = $(this).parents('#old-forgot'); var form = $(this).parents('form[name="old-forgot"]'); var p = {username:$('#forgot_username', $forgotContainer).val().trim()}; var $calloutContainer = $(this).parents('.callout-container.login .callout'); $('button', form).hide(); $('.error', $forgotContainer).remove(); $('.spinner', form).show(); $calloutContainer.animate({'height': $forgotContainer.outerHeight()}, 100, 'linear', function() { $.post(form.attr('action'), p, function(response) { response = JSON.parse(response); $('.spinner', form).hide(); if(response.status == 'success'){ $calloutContainer.find('.password-success-message').html('Done! ' + response.data.message + "
").show(); $forgotContainer.fadeOut(100, 'linear', function() { $calloutContainer.animate({'height': $calloutContainer.find('#old-login-lightbox').outerHeight()}, 100, 'linear', function() { $calloutContainer.find('#old-login-lightbox').fadeTo(100, 1, 'linear'); }) $('button', form).show(); }); } else{ $('.error', $forgotContainer).remove(); $('#forgot_username', form).after('' + response.data.message + "
"); } else{ $('.error').remove(); $('form #forgot_username', $forgotContainer).after('