horizon/horizon/static/horizon/js/horizon.js

369 lines
13 KiB
JavaScript

/* This is the base Horizon JavaScript object. There is only ever one of these
* loaded (referenced as horizon with a lower-case h) which happens immediately
* after the definition below.
*
* Scripts that are dependent on functionality defined in the Horizon object
* must be included after this script in templates/base.html.
*/
var Horizon = function() {
var horizon = {};
var initFunctions = [];
/* Use the addInitFunction() function to add initialization code which must
* be called on DOM ready. This is useful for adding things like event
* handlers or any other initialization functions which should preceed user
* interaction but rely on DOM readiness.
*/
horizon.addInitFunction = function(fn) {
initFunctions.push(fn);
};
/* Call all initialization functions and clear the queue. */
horizon.init = function() {
// Load client-side template fragments and compile them.
horizon.templates.compile_templates();
// Bind event handlers to confirm dangerous actions.
$("body").on("click", "form button.btn-danger", function (evt) {
horizon.datatables.confirm(this);
evt.preventDefault();
});
// Bind dismiss(x) handlers for alert messages.
$(".alert").alert();
$.each(initFunctions, function(ind, fn) {
fn();
});
// Prevent multiple executions, just in case.
initFunctions = [];
};
/* Convenience functions for dealing with namespaced Horizon cookies. */
horizon.cookies = {
read: function (cookie_name) {
// Read in a cookie which contains JSON, and return a parsed object.
var cookie = $.cookie("horizon." + cookie_name);
if (cookie === null) {
return {};
}
return $.parseJSON(cookie);
},
write: function (cookie_name, data) {
// Overwrites a cookie.
$.cookie("horizon." + cookie_name, JSON.stringify(data), {path: "/"});
},
update: function (cookie_name, key, value) {
var data = horizon.cookies.read("horizon." + cookie_name);
data[key] = value;
horizon.cookies.write(cookie_name, data);
},
delete: function (cookie_name) {
$.cookie("horizon." + cookie_name, null);
}
};
/* Namespace for core functionality related to Forms. */
horizon.forms = {
handle_source_group: function() {
// Delegate this handler to form, so it only should be init once
$("form").live("change", "#id_source_group", function(evt) {
var $sourceGroup = $(this).find('#id_source_group');
var $cidrContainer = $(this).find('#id_cidr').parent().parent();
if($sourceGroup.val() === "") {
$cidrContainer.removeClass("hide");
} else {
$cidrContainer.addClass("hide");
}
});
}
};
/* Namespace for core functionality related to DataTables. */
horizon.datatables = {
update: function () {
var $rows_to_update = $('tr.status_unknown.ajax-update');
if ($rows_to_update.length) {
var interval = $rows_to_update.attr('data-update-interval'),
$table = $rows_to_update.closest('table'),
decay_constant = $table.attr('decay_constant');
// Do not update this row if the action column is expanded
if ($rows_to_update.find('.actions_column .btn-group.open').length) {
// Wait and try to update again in next interval instead
setTimeout(horizon.datatables.update, interval);
// Remove interval decay, since this will not hit server
$table.removeAttr('decay_constant');
return;
}
// Trigger the update handlers.
$rows_to_update.each(function(index, row) {
var $row = $(this),
$table = $row.closest('table');
horizon.ajax.queue({
url: $row.attr('data-update-url'),
error: function (jqXHR, textStatus, errorThrown) {
switch (jqXHR.status) {
// A 404 indicates the object is gone, and should be removed from the table
case 404:
// Update the footer count and reset to default empty row if needed
var $footer = $table.find('tr:last'),
row_count, footer_text, colspan, template, params, $empty_row;
// existing count minus one for the row we're removing
row_count = $table.find('tbody tr').length - 1;
footer_text = "Displaying " + row_count + " item";
if(row_count !== 1) {
footer_text += 's';
}
$footer.find('span').text(footer_text);
if(row_count === 0) {
colspan = $footer.find('td').attr('colspan');
template = horizon.templates.compiled_templates["#empty_row_template"];
params = {"colspan": colspan};
empty_row = template.render(params);
$row.replaceWith(empty_row);
} else {
$row.remove();
}
break;
default:
if (horizon.conf.debug) {
horizon.alert("error", "An error occurred while updating.");
}
$row.removeClass("ajax-update");
$row.find("i.ajax-updating").remove();
break;
}
},
success: function (data, textStatus, jqXHR) {
var $new_row = $(data);
if($new_row.hasClass('status_unknown')) {
// only add spinning animation if row needs update
var spinner = '<i class="icon-updating ajax-updating"></i>';
$new_row.find("td.status_unknown").prepend(spinner);
}
// Only replace row if the html content has changed
if($new_row.html() != $row.html()) {
if($row.find(':checkbox').is(':checked')) {
// Preserve the checkbox if it's already clicked
$new_row.find(':checkbox').prop('checked', true);
}
$row.replaceWith($new_row);
// Reset decay constant.
$table.removeAttr('decay_constant');
}
},
complete: function (jqXHR, textStatus) {
// Revalidate the button check for the updated table
horizon.datatables.validate_button();
}
});
});
// Set interval decay to this table, and increase if it already exist
if(decay_constant === undefined) {
decay_constant = 1;
} else {
decay_constant++;
}
$table.attr('decay_constant', decay_constant);
// Poll until there are no rows in an "unknown" state on the page.
next_poll = interval * decay_constant;
// Limit the interval to 30 secs
if(next_poll > 30 * 1000) next_poll = 30 * 1000;
setTimeout(horizon.datatables.update, next_poll);
}
},
validate_button: function () {
// Disable form button if checkbox are not checked
$("form").each(function (i) {
var checkboxes = $(this).find(":checkbox");
if(!checkboxes.length) {
// Do nothing if no checkboxes in this form
return;
}
if(!checkboxes.filter(":checked").length) {
$(this).find(".table_actions button.btn-danger").addClass("disabled");
}
});
$("form :checkbox").on("click", function (evt) {
var $form = $(this).closest("form");
var any_checked = $form.find("tbody :checkbox").is(":checked");
// Enable the button if any checkbox is checked,
// Disable if all checkbox is cleared
if(any_checked) {
$form.find(".table_actions button.btn-danger").removeClass("disabled");
}else {
$form.find(".table_actions button.btn-danger").addClass("disabled");
}
});
}
};
/* Generates a confirmation modal dialog for the given action. */
horizon.datatables.confirm = function (action) {
var $action = $(action),
$modal_parent = $(action).closest('.modal'),
action_string, title, body, modal, form;
if($action.hasClass("disabled")) {
return;
}
action_string = $action.text();
title = "Confirm " + action_string;
body = "Please confirm your selection. This action cannot be undone.";
modal = horizon.modals.create(title, body, action_string);
modal.modal();
if($modal_parent.length) {
var child_backdrop = modal.next('.modal-backdrop');
// re-arrange z-index for these stacking modal
child_backdrop.css('z-index', $modal_parent.css('z-index')+10);
modal.css('z-index', child_backdrop.css('z-index')+10);
}
modal.find('.btn-primary').click(function (evt) {
form = $action.closest('form');
form.append("<input type='hidden' name='" + $action.attr('name') + "' value='" + $action.attr('value') + "'/>");
form.submit();
modal.modal('hide');
return false;
});
return modal;
};
/* Namespace for core functionality related to client-side templating. */
horizon.templates = {
template_ids: ["#modal_template", "#empty_row_template", "#alert_message_template"],
compiled_templates: {}
};
/* Pre-loads and compiles the client-side templates. */
horizon.templates.compile_templates = function () {
$.each(horizon.templates.template_ids, function (ind, template_id) {
horizon.templates.compiled_templates[template_id] = Hogan.compile($(template_id).text());
});
};
/* Namespace for core functionality related to modal dialogs. */
horizon.modals = {};
/* Creates a modal dialog from the client-side template. */
horizon.modals.create = function (title, body, confirm, cancel) {
if (!cancel) {
cancel = "Cancel";
}
var template = horizon.templates.compiled_templates["#modal_template"],
params = {title: title, body: body, confirm: confirm, cancel: cancel},
modal = $(template.render(params)).appendTo("body");
return modal;
};
/* Utilities for common needs which aren't JS builtins. */
horizon.utils = {
capitalize: function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
};
horizon.instances = {
user_decided_length: false,
getConsoleLog: function(form_element, via_user_submit) {
if(this.user_decided_length) {
var data = $(form_element).serialize();
} else {
var data = "length=35";
}
$.ajax({
url: $(form_element).attr('action'),
data: data,
method: 'get',
success: function(response_body) {
$('pre.logs').html(response_body);
},
error: function(response) {
if(via_user_submit) {
horizon.clearErrorMessages();
horizon.alert('error', 'There was a problem communicating with the server, please try again.');
}
}
});
}
};
horizon.alert = function (type, message) {
var template = horizon.templates.compiled_templates["#alert_message_template"],
params = {"type": type,
"type_capitalized": horizon.utils.capitalize(type),
"message": message};
return $(template.render(params)).prependTo("#main_content .messages");
};
horizon.clearErrorMessages = function() {
$('#main_content .messages .alert.alert-error').remove()
};
/* Queued ajax handling for Horizon.
*
* Note: The number of concurrent AJAX connections hanlded in the queue
* can be configured by setting an "ajax_queue_limit" key in
* settings.HORIZON_CONFIG to the desired number (or None to disable queue
* limiting).
*/
horizon.ajax = {
// This will be our jQuery queue container.
_queue: [],
_active: [],
// Function to add a new call to the queue.
queue: function(opts) {
var complete = opts.complete,
active = horizon.ajax._active;
opts.complete = function () {
var index = $.inArray(request, active);
if (index > -1) {
active.splice(index, 1);
}
horizon.ajax.next();
if (complete) {
complete.apply(this, arguments);
}
};
function request() {
return $.ajax(opts);
}
// Queue the request
horizon.ajax._queue.push(request);
// Start up the queue handler in case it's stopped.
horizon.ajax.next();
},
next: function () {
var queue = horizon.ajax._queue,
limit = horizon.conf.ajax.queue_limit,
request;
if (queue.length && (!limit || horizon.ajax._active.length < limit)) {
request = queue.pop();
horizon.ajax._active.push(request);
return request();
}
}
};
return horizon;
};
// Create the one and only horizon object.
var horizon = Horizon();
// Call init on DOM ready.
$(document).ready(horizon.init);