/* * Base.js * Author: Adam Bratt * Date: February 22, 2012 * Description: * Base.js setups functions that will be included on every page load * */ Mkfy = window.Mkfy || {}; //////////////////////////////////////////////////////////////////// //////////////////// Built-in Tools ///////////////////// //////////////////////////////////////////////////////////////////// !function(window, document, mkfy, undefined){ /** * setupForm() establishes a standard method of handling all AJAX forms. * * Every form should have the same types of errors and same JSON format * returned by the Django server. * * The options argument accepts a bunch of different functions that modify * behavior of the forms callbacks. * */ mkfy.setupForm = function(options){ if (typeof options == 'function'){ options = { success: options }; } options = $.extend(true, { }, options); }; /** * subscribe() allows for multiple places to subscribe to incoming alerts * * Incoming alerts are currently coming in from the PubNub backend so this is * where the code attaches to. It could be shifted to any other service in the * future though and would only need to be changed here */ mkfy.subscriptions = {}; mkfy.subscribe = function (channel, fn, tag) { tag = tag || 'all'; if (mkfy.subscriptions[channel]) { if (mkfy.subscriptions[channel][tag]) { mkfy.subscriptions[channel][tag].push(fn); } else { mkfy.subscriptions[channel][tag] = [fn]; } } else { pubnub.subscribe({ channel: channel, message: createSubscribeCallback(channel) }); mkfy.subscriptions[channel] = {}; mkfy.subscriptions[channel][tag] = [fn]; } }; var createSubscribeCallback = function (channel) { return function (data) { if (!mkfy.subscriptions[channel]) return; // Call any subscribed on the all callback if (mkfy.subscriptions[channel].hasOwnProperty('all')) { var all = mkfy.subscriptions[channel].all; for (var i=0; i < all.length; i++) { all[i](data.content); } } // Call any tagged callbacks if (data.hasOwnProperty('tag')) { if (mkfy.subscriptions[channel].hasOwnProperty(data.tag)) { var tagged = mkfy.subscriptions[channel][data.tag]; for (var i=0; i < tagged.length; i++) { tagged[i](data.content); } } } } }; /** * notify() shows a new notification in the bottom left of the window * * Eventually we will add a full object API to interact with the notification * system. For now, it just does a simple job. * * time - milliseconds till fade out * message - text of notification * title - title of notification */ mkfy.notify = function (options) { options.title = options.title || 'Notification'; options.time = options.time || 7500; options.expires = true; // TODO: make it not fade out options.url = options.url || '/'; var html = $('
'); html.on('click', function () { window.open(window.location.origin+options.url); return false; }); $('
×
').on('click', function(e) { $(this).parents('.notification').addClass('flipOutX'); setTimeout(function () { $(this).remove(); }, 1500); e.stopPropagation(); }).appendTo(html); html.append('
'+options.title+'
'); html.append('

'+options.message+'

'); $('body').append(html); setTimeout(function (){ html.addClass('flipOutX'); setTimeout(function () { $(this).remove(); }, 1500); }, options.time); } }(window, document, Mkfy); if (typeof console === "undefined" || typeof console.log === "undefined") { console = {log: function() {} }; } console.log('It looks like you know what you\'re doing, send an email to dev-jobs@marketfy.com!'); function ajaxForm(form_id, callback, error_callback){ var form = $('#'+form_id); clear_form_errors(form); form.find('[type=submit]').attr('disabled',true); var url = form.attr('action'); /* Internet Explorer does not like these. */ if (url == "" || url == "#") url = "."; $.post(url, loadForm(form), function(data){ setTimeout(function(){form.find('[type=submit]').removeAttr('disabled')}, 500); if(form_errors(form, data)){ if(typeof error_callback != "undefined"){ error_callback(data); } return; } if(data.url){ window.location = data.url; } if(typeof callback != "undefined"){ callback(data); } }).error(function(jqXHR, exception){ var issue = ''; if (jqXHR.status === 0) { issue = 'Not connected. Verify internet connection.'; } else if (jqXHR.status == 404) { issue = 'Requested page not found. [404]'; } else if (jqXHR.status == 500) { issue = 'Internal Server Error [500].'; } else if (exception === 'parsererror') { issue = 'Requested JSON parse failed.'; } else if (exception === 'timeout') { issue = 'Time out error.'; } else if (exception === 'abort') { issue = 'Ajax request aborted.'; } else { issue = 'Uncaught Error, ' + jqXHR.responseText; } form.find('[type=submit]').removeAttr('disabled'); alert('Crikey! A server-side error occurred.\n\nOur tech guys are getting text messages and emails with this issue as you read this. \n\nTry submitting the form again and if you still get this error wait a few hours and our resident nerds should have it taken care of.\n\nError Message: '+issue+' \n\nThanks for your patience,\nMarketfy Nerd Patrol'); }); } function ajaxExtraForm(form_id, extra_attrs, callback, error_callback ){ form = $('#'+form_id); clear_form_errors(form); form.find('[type=submit]').attr('disabled',true); // Modify to pass extra attributes extra_attrs = extra_attrs || {}; post_values = $.extend({}, loadForm(form), extra_attrs); $.post(form.attr('action'), post_values, function(data){ setTimeout(function(){form.find('[type=submit]').removeAttr('disabled')}, 500); if(form_errors(form, data)){ if(typeof error_callback != "undefined"){ error_callback(data); } return; } if(data.url){ window.location = data.url; } if(typeof callback != "undefined"){ callback(data); } }).error(function(jqXHR, exception){ var issue = ''; if (jqXHR.status === 0) { issue = 'Not connected. Verify internet connection.'; } else if (jqXHR.status == 404) { issue = 'Requested page not found. [404]'; } else if (jqXHR.status == 500) { issue = 'Internal Server Error [500].'; } else if (exception === 'parsererror') { issue = 'Requested JSON parse failed.'; } else if (exception === 'timeout') { issue = 'Time out error.'; } else if (exception === 'abort') { issue = 'Ajax request aborted.'; } else { issue = 'Uncaught Error, ' + jqXHR.responseText; } form.find('[type=submit]').removeAttr('disabled'); alert('Crikey! A server-side error occurred.\n\nOur tech guys are getting text messages and emails with this issue as you read this. \n\nTry submitting the form again and if you still get this error wait a few hours and our resident nerds should have it taken care of.\n\nError Message: '+issue+' \n\nThanks for your patience,\nMarketfy Nerd Patrol'); }); } function loadForm(element){ var form_vars = {}; element.find('[name]').each(function(){ var n = $(this).attr('name'); if(!n.length) return; if(($('[name='+n+']')).length > 1 && $(this).attr('type') == "checkbox") var is_multiple = true; //Skip over unchecked radios/checkboxes if( $(this).attr('type') == "checkbox" || $(this).attr('type') == "radio" ){ if (!$(this).prop('checked')) return; } if(is_multiple){ // It already exists in here so now make an array if(form_vars[n]){ form_vars[n].push($(this).val()); } else { form_vars[n] = Array($(this).val()); } }else { form_vars[n] = $(this).val(); } }); return form_vars; } function form_errors(form, json, tooltips_enabled) { var has_error = false; // OLD: Legacy way if(json.error && json.error.length){ has_error = true; form.prepend('
'+json.error+'
'); } if(json.errors){ has_error = true; for(e in json.errors){ if (e == '__all__' || e == 'non_field_errors'){ form.prepend('
'+json.errors[e][0]+'
'); continue; } var input = form.find('[name='+e+']'); var error_msg = $("").addClass("help-inline ajax-error alert-error").text(json.errors[e][0]); input.addClass("error"); error_msg.insertAfter(input); } } // NEW: Proper way we will handle errors if(json.status == 'error'){ if(json.data){ has_error = true; $.each(json.data, function(k,v){ var input = form.find('[name='+k+']'); input.addClass('error'); input.parents('.control-group').addClass('error'); }); } if(json.msg){ has_error = true; form.prepend('
'+json.msg+'
'); } } return has_error; } function clear_form_errors(form) { form.find('.alert-error').remove(); form.find('.ajax-error').remove(); form.find('.error').removeClass('error'); form.find('.tooltip-error').remove(); } //////////////////////////////////////////////////////////////////// //////////////////// Templates ///////////////////// //////////////////////////////////////////////////////////////////// BaseTemplateEngine = function(compiler){ this.compiler = compiler; this.templates = []; }; BaseTemplateEngine.prototype.render = function(template, context){ if (this.templates[template] === undefined){ var content = $('#'+template).html(); if(content){ this.templates[template] = Handlebars.compile(content); } else { return ''; } } return this.templates[template](context); }; BaseTemplateEngine.prototype.addHandler = function(handlerName, func){ return Handlebars.registerHelper(handlerName, func); }; /* TODO: Include handlebars.js before this or somehow combine the scripts so that we can pass in Handlebars as the compiler */ TemplateEngine = new BaseTemplateEngine(); //////////////////////////////////////////////////////////////////// //////////////////// jQuery ///////////////////// //////////////////////////////////////////////////////////////////// $(function(){ // Custom Checkbox Activate $('.checkbox-custom input').change(function () { var e = $(this).parents('.checkbox-custom'); if ($(this).prop('checked')) { e.addClass('active'); } else { e.removeClass('active'); } }); // No idea what this is trying to do but it's causing an error //if (window.location.hash) { // var h = window.location.hash; // var res = $(h); // if(res.length && res.hasClass('modal')){ // res.modal(); // } //} // Feedback $('#feedback-suggestions').focus(function(){ $(this).animate({'height': 80}, function(){ $('#feedback-submit').removeClass('hide'); }); }).blur(function(){ if($(this).val() == ''){ $(this).animate({'height': 25}, function(){ $('#feedback-submit').addClass('hide'); }); } }); $('#feedback-form').submit(function(){ var self = $(this); ajaxForm('feedback-form', function(ret){ if(ret.status == 'ok'){ self.find('textarea,button').remove(); self.find('h3').after('
Thanks for your feedback!
'); } }); return false; }); $(document.links).filter(function () { return this.hostname != window.location.hostname; }).attr('target', '_blank'); }); function clickylog(href, title, type){ if(typeof clicky != 'undefined'){ clicky.log(href, title, type); } } function setupPieChart(element_id, data){ return { chart: { renderTo: element_id }, colors: ['#5D6C9A', '#6AB77E', '#E4C47E', '#AC5F95', '#909FCD', '#D68EC0', '#D7EC9D', '#F2D8A1'], title: { text: null }, tooltip: { formatter: function() { return ''+ this.point.name +': '+ Math.round(this.percentage*100)/100 +' %'; } }, plotOptions: { pie: { allowPointSelect: true, cursor: 'pointer', dataLabels: { enabled: true, distance: -30, color: "#fff", style: { textShadow: "0px 1px 1px #666", fontWeight: "bold" } }, size: '95%' } }, series: [{ type: 'pie', name: 'Pie Chart', data: data }] }; } function setupPerformanceChart(element_id, data){ return { chart: { renderTo: element_id, type: 'line', plotBorderWidth: 1, style: { fontFamily: 'proxima-nova, sans-serif !important', fontWeight: 'bold', color: '#222' } }, colors: ['#5D6C9A', '#6AB77E', '#E4C47E', '#AC5F95', '#909FCD', '#D68EC0', '#D7EC9D', '#F2D8A1'], plotOptions: { }, navigator: { enabled: false }, rangeSelector: { enabled: false }, xAxis: { type: 'datetime', dateTimeLabelFormats: { day: '%b of %Y' }, labels: { align: 'center', style: { fontFamily: 'proxima-nova, sans-serif !important', fontWeight: 'bold', color: '#222' } }, tickLength: 0, gridLineWidth: 0, lineWidth: 0, ordinal: false, offset: 5, tickPixelInterval: 180 }, yAxis: { labels: { formatter: function() { return this.value + ' %'; } } }, tooltip: { xDateFormat: '%Y-%m-%d', useHTML: true, formatter: function () { var s = '' + Highcharts.dateFormat('%b %e, %Y', this.x) + ''; $.each(this.points, function(i, point) { s += '
' + point.series.name + ' ' + point.y.toFixed(2) + ' %'; }); return s; } }, scrollbar: { enabled: false }, series: [{ name: 'Portfolio', data: data.portfolio}, { name: 'S&P 500', data: data.standard}] }; } // GET Request Vars function getQueryParams(qs) { qs = qs.split("+").join(" "); var params = {}, tokens, re = /[?&]?([^=]+)=([^&]*)/g; while (tokens = re.exec(qs)) { params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]); } return params; } $_GET = getQueryParams(document.location.search); // Sets up origin to contain base URL if (!window.location.origin) window.location.origin = window.location.protocol+"//"+window.location.host; // Allow posting of messages from new windows in IE8 function postMessageInterface(str) { window.top.postMessage(str, window.location.origin); } // // Handles CSRF Tokens for AJAX // function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } var csrftoken = getCookie(CSRF_COOKIE_NAME); function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({ crossDomain: false, // obviates need for sameOrigin test beforeSend: function(xhr, settings) { if (!csrfSafeMethod(settings.type)) { xhr.setRequestHeader("X-CSRFToken", getCookie(CSRF_COOKIE_NAME)); } } }); // Setup jQuery Extenders (function($){ // jQuery Extending $.fn.extend({ rest: function (options) { this.each(function () { restRequest.apply(this, [options]); }); }, restForm: function (options) { this.each(function () { createRestForm.apply(this, [options]) }); } }); var restRequest = function (options) { /* TODO: - Finish documentation - Make restForm based off of this - Make it work for $.rest direct calls (no element) */ var self = $(this); var settings = { url: self.attr('href') || self.data('url'), method: self.data('method') || 'post', timeout: self.data('timeout') || 30000, data: {}, success: function () { }, error: function () { } }; // Check to see if we were passed in a function if (typeof(options) === 'function') { settings.success = options; } else { // Overwrite settings with supplied options $.extend(settings, options); } $.ajax({ url: settings.url, type: settings.method, timeout: settings.timeout, contentType: 'application/json', dataType: 'json', data: settings.data, success: function (resp) { settings.success.apply(self, [resp]); }, error: function (jqXHR, statusText) { settings.error.apply(self, [jqXHR, statusText]); } }); }; var createRestForm = function (options) { /* * restForm * * This extension makes it easy to use REST endpoints with Bootstrap forms * * To initialize set data-form-handler="rest" as a form attribute or use: * * $('#form-id').restForm(options); * * Where options is an object containing overrides for the settings * variable defined below. * * @url 'string' URL to which the form data will be submitted * @type 'string' Method type - POST, PUT, DELETE * @excludeFields [array] List of field names to exclude from data processing * @data {object} Additional hardcoded data to include in the request * @timeout (int) Milliseconds before request should timeout * @disableSubmit (bool) Disables submit button until request returns * @formErrorTarget (ele) Where to prepend any form-wide errors * * * TODO: We could make some of the field handling methods more CPU efficient * by using elements instead of field names. * */ var self = $(this); var settings = { url: self.attr('action'), method: self.attr('method'), excludeFields: [], data: {}, timeout: 30000, disableSubmit: true, formErrorTarget: null, scrollToError: false, /* Custom field handlers in the format: { password: { clear: function () { ... Code to clear errors before re-submitting ... }, error: function (errors) { ... Code to handle and render errors ... }, data: function () { ... Code to return the data value of this field ... } } } */ fieldHandlers: {}, // Prepares data to be submitted, takes data and returns processed data prepareData: function (dataObj) { return dataObj; }, // Serializes form data - by default into JSON serializeData: function (data) { return JSON.stringify(data); }, // Runs before the form is submitted via AJAX beforeSubmit: function () { }, // Called immediately after AJAX is returned afterSubmit: function () { }, // Clear overall form errors, Everything that's not a field clearFormErrors: function () { self.find('.form-error.alert-error').remove(); }, // Render overall form errors, Everything that's not a field renderFormErrors: function (errorArray) { if (typeof errorArray === 'string') { errorArray = [errorArray]; } var errorHTML = $("
").addClass('form-error alert alert-error').html(errorArray.join('
')); if (settings.formErrorTarget) { errorHTML.prependTo(settings.formErrorTarget); } else { errorHTML.prependTo(self); } }, // Clear errors on a field clearFieldErrors: function (fieldName) { var controlGroup = self.find('[name='+fieldName+']').parents('.control-group'); if (controlGroup.length) { controlGroup.removeClass('error'); controlGroup.find('.alert-error').remove(); } else { self.find('[name='+fieldName+']').siblings('.alert-error').remove(); } }, // Renders errors for a field renderFieldErrors: function (fieldName, errorArray) { var field = self.find('[name='+fieldName+']'); var errorHTML = $("").addClass("help-inline ajax-error alert-error").html(errorArray.join('
')); field.parents('.control-group').addClass('error'); errorHTML.insertAfter(field); }, // Called on success success: function (data) { }, // Called with errorObject right before errors are process beforeError: function (errorObject) { }, // Fired after errors are processed afterError: function (error, level, jqXHR) { // Level is 0 by default, if it's higher you could log using sentry }, // Fired on error if scrollToError setting is true errorScroll: function (errorObject) { var errorElement = $('.error,.form-error,.alert-error').first(); if (errorElement) { $('html, body').animate({ scrollTop: Math.max(errorElement.offset().top - 150, 0) }, 1000); } } }; /********************** Initialization ************************/ // Use form error target if supplied with proper CSS attribute var formErrorTarget = self.find('[data-form-target="error"]'); if (formErrorTarget.length) { settings.formErrorTarget = formErrorTarget[0]; } // Check to see if we were passed in a function if (typeof(options) === 'function') { settings.success = options; } else { // Overwrite settings with supplied options $.extend(settings, options); } /******************** Built-in Handlers **********************/ var getFormData = function () { /* * Prepares form data object prior to serialization * * return @dataObj */ // Load array of form fields var dataObj = {}; var formData = self.serializeArray(); // Loop through each form element for (var i = 0; i < formData.length; i++) { // Check to make sure this field isn't excluded if (!settings.excludeFields.length && $.inArray(formData[i].name,settings.excludeFields) == -1) { dataObj[formData[i].name] = formData[i].value; } } // Loop through custom fieldHandlers to add in data for custom fields $.each(settings.fieldHandlers, function (k, v) { if (v.data) { dataObj[k] = v.data.apply(self); } }); // Include extra data $.extend(dataObj, settings.data); // Run through custom prepareData function dataObj = settings.prepareData.apply(self, [dataObj]); return dataObj; }; var beforeSubmitCallback = function () { /* * Handles preparing the form before the submit process * */ // Disable submit buttons until data is returned if (settings.disableSubmit) { self.find('[type=submit]').attr('disabled', 'disabled'); } // Clear form errors settings.clearFormErrors.apply(self); self.find('[name]').each( function () { var name = $(this).attr('name'); if (settings.fieldHandlers[name] && settings.fieldHandlers[name].hasOwnProperty('clear')) { settings.fieldHandlers[name].clear.apply(self) } else { settings.clearFieldErrors.apply(self, [name]); } }); settings.beforeSubmit.apply(self); }; var afterSubmitCallback = function () { /* * Handles misc changes to the form after a response is returned * */ // Re-enable the submit buttons if (settings.disableSubmit) { // Give some time to render any form changes setTimeout(function () { self.find('[type=submit]').removeAttr('disabled') }, 200); } settings.afterSubmit.apply(self); }; var errorsCallback = function (errorObject) { /* * Called on AJAX errors that pass back a JSON object of fields * * @errorObject JSON object containing {field: [errors]} */ settings.beforeError.apply(self, [errorObject]); $.each(errorObject, function (k, v) { // For form-level errors (not field) we use __all__ like Django Forms do if (k === '__all__' || k == 'non_field_errors' || k == 'errors') { settings.renderFormErrors.apply(self, [v]); return; } // Check to see if custom error handler exists if (settings.fieldHandlers[k] && settings.fieldHandlers[k].error) { settings.fieldHandlers[k].error.apply(self, [v]); } else { // Use default error renderer settings.renderFieldErrors.apply(self, [k, v]); } }); if(settings.scrollToError) { settings.errorScroll.apply(self, [errorObject]); } }; /************************* Helper Functions *********************/ var getJSONObject = function (jsonString) { /* * Makes sure that only a JSON object is returned * * return {obj} or false */ try { var obj = $.parseJSON(jsonString); } catch (e) { return false; } if (obj === null || typeof obj !== 'object') { return false; } return obj; }; /*************** Handler on the .submit() listener **************/ self.submit(function (e) { // Stop event propogation just to be safe e.stopPropagation(); // Get form data var data = settings.serializeData.apply(self, [getFormData()]); // Call pre-submit handler if (beforeSubmitCallback() === false) { return false; } // Here it is, the holy grail. The actual request!!! $.ajax({ url: settings.url, type: settings.method, timeout: settings.timeout, contentType: 'application/json', dataType: 'json', data: data, success: function (resp) { // Call post-submit handler afterSubmitCallback(); settings.success.apply(self, [resp]); }, error: function (jqXHR, statusText) { // Call post-submit handler afterSubmitCallback(); // See if this is just a normal HTTP error (will have JSON) var errors = getJSONObject(jqXHR.responseText); if (errors !== false) { errorsCallback(errors); settings.afterError.apply(self, [statusText, 0, jqXHR]); return; } // Handle common HTTP errors if (statusText == 'error') { var status = ''; switch (jqXHR.status) { case 500: case 502: case 503: status = 'Server Error. Please contact support!'; break; case 404: status = 'The resource you requested does not exist.'; break; case 403: status = 'You do not have permission to perform this action.' break; case 0: status = 'Internet connection Lost.' break; default: status = 'Uncaught error. Please contact support!'; break; } errorsCallback({'__all__': [status]}); settings.afterError.apply(self, [status, 2, jqXHR]); // Check to see if it's an easy to define issue } else { switch (statusText) { case 'parsererror': // Could not parse JSON form a normal response case 'timeout': // Request timed out case 'abort': // Ajax request aborted default: // Uncaught error errorsCallback({'__all__': [statusText]}); settings.afterError.apply(self, [statusText, 2, jqXHR]); break; } } // Log it because this is not an ordinary error console.log('AJAX Exception: ' + statusText); console.log(jqXHR); } }); // Need this in addition to stopping event propagation return false; }); }; })(jQuery); /* * djangoURL * * Allows us to use a django URL in javascript. Only works as long as there are * not defined numbers in the URL structure. * * example usage: * * var modulePage = djangoURL("{% url 'product' 1 2 %}"); * window.location = modulePage('test-product', 33); * * */ function djangoURL(baseURL) { return function () { var url = baseURL; for (var i = 0; i < arguments.length; i++) { var argIndex = url.lastIndexOf(i+1); if (argIndex != -1) { url = url.substr(0, argIndex) + arguments[i] + url.substr(argIndex + i.toString().length); } } return url; } }