539 lines
18 KiB
JavaScript
539 lines
18 KiB
JavaScript
/**
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
* not use this file except in compliance with the License. You may obtain
|
|
* a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
/* Namespace for core functionality related to modal dialogs.
|
|
*
|
|
* Modals in Horizon are treated as a "stack", e.g new ones are added to the
|
|
* top of the stack, and they are always removed in a last-in-first-out
|
|
* order. This allows for things like swapping between modals as part of a
|
|
* workflow, for confirmations, etc.
|
|
*
|
|
* When a new modal is loaded into the DOM, it fires a "new_modal" event which
|
|
* event handlers can listen for. However, for consistency, it is better to
|
|
* add methods which should be run on instantiation of any new modal to be
|
|
* applied via the horizon.modals.addModalInitFunction method.
|
|
*/
|
|
horizon.modals = {
|
|
// Storage for our current jqXHR object.
|
|
_request: null,
|
|
spinner: null,
|
|
progress_bar: null,
|
|
_init_functions: []
|
|
};
|
|
|
|
horizon.modals.addModalInitFunction = function (f) {
|
|
horizon.modals._init_functions.push(f);
|
|
};
|
|
|
|
horizon.modals.initModal = function (modal) {
|
|
$(horizon.modals._init_functions).each(function (index, f) {
|
|
f(modal);
|
|
});
|
|
};
|
|
|
|
/* Creates a modal dialog from the client-side template. */
|
|
horizon.modals.create = function (title, body, confirm, cancel, confirmCssClass) {
|
|
if (!cancel) {
|
|
cancel = gettext("Cancel");
|
|
}
|
|
var template = horizon.templates.compiled_templates["#modal_template"],
|
|
params = {
|
|
title: title,
|
|
body: body,
|
|
confirm: confirm,
|
|
cancel: cancel,
|
|
confirmCssClass: confirmCssClass || "btn-primary",
|
|
modal_backdrop: horizon.modals.MODAL_BACKDROP
|
|
};
|
|
return $(template.render(params)).appendTo("#modal_wrapper");
|
|
};
|
|
|
|
horizon.modals.success = function (data) {
|
|
var modal;
|
|
var modal_wrapper = $('#modal_wrapper');
|
|
// Moves the modal_wrapper to the bottom. This shows it over other dialogs.
|
|
modal_wrapper.parent().append(modal_wrapper);
|
|
modal_wrapper.append(data);
|
|
modal = $('#modal_wrapper > .modal:last');
|
|
modal.modal();
|
|
$(modal).trigger("new_modal", modal);
|
|
return modal;
|
|
};
|
|
|
|
horizon.modals.modal_spinner = function (text, $container) {
|
|
if (!$container) {
|
|
$container = $('#modal_wrapper');
|
|
}
|
|
|
|
// Adds a spinner with the desired text in a modal window.
|
|
horizon.modals.spinner = horizon.loader.modal(text);
|
|
horizon.modals.spinner.appendTo($container);
|
|
horizon.modals.spinner.modal({backdrop: 'static'});
|
|
};
|
|
|
|
horizon.modals.progress_bar = function (text) {
|
|
var template = horizon.templates.compiled_templates["#progress-modal"];
|
|
horizon.modals.bar = $(template.render({text: text}))
|
|
.appendTo("#modal_wrapper");
|
|
horizon.modals.bar.modal({backdrop: 'static'});
|
|
|
|
var $progress_bar = horizon.modals.bar.find('.progress-bar');
|
|
horizon.modals.progress_bar.update = function(fraction) {
|
|
var percent = Math.round(100 * fraction) + '%';
|
|
$progress_bar
|
|
.css('width', Math.round(100 * fraction) + '%')
|
|
.parents('.progress-text').find('.progress-bar-text').text(percent);
|
|
};
|
|
};
|
|
|
|
horizon.modals.init_wizard = function () {
|
|
// If workflow is in wizard mode, initialize wizard.
|
|
var _max_visited_step = 0;
|
|
var _validate_steps = function (start, end) {
|
|
var $form = $('.workflow > form'),
|
|
response = {};
|
|
|
|
if (typeof end === 'undefined') {
|
|
end = start;
|
|
}
|
|
|
|
// Clear old errors.
|
|
if ($form.find('div.row div.alert-danger').siblings().length == 0) {
|
|
$form.find('div.row div.alert-danger').parent().remove();
|
|
} else {
|
|
$form.find('div.row div.alert-danger').remove();
|
|
}
|
|
$form.find('.form-group.has-error').each(function () {
|
|
var $group = $(this);
|
|
$group.removeClass('has-error');
|
|
$group.find('span.help-block').remove();
|
|
});
|
|
|
|
// Temporarilly remove "disabled" attribute to get the values serialized
|
|
var disabled = $form.find('select[disabled="disabled"]').prop("disabled", false);
|
|
|
|
// Send the data for validation.
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: $form.attr('action'),
|
|
headers: {
|
|
'X-Horizon-Validate-Step-Start': start,
|
|
'X-Horizon-Validate-Step-End': end
|
|
},
|
|
data: $form.serialize(),
|
|
dataType: 'json',
|
|
async: false,
|
|
success: function (data) { response = data; }
|
|
});
|
|
|
|
// Re-enable the "disabled" attribute on disabled inputs.
|
|
disabled.attr("disabled", "disabled");
|
|
|
|
// Handle errors.
|
|
if (response.has_errors) {
|
|
var first_field = true;
|
|
|
|
$.each(response.errors, function (step_slug, step_errors) {
|
|
var step_id = response.workflow_slug + '__' + step_slug,
|
|
$fieldset = $form.find('#' + step_id);
|
|
$.each(step_errors, function (field, errors) {
|
|
var $field;
|
|
if (field === '__all__') {
|
|
// Add global errors.
|
|
$.each(errors, function (index, error) {
|
|
$fieldset.find('div.row').prepend(
|
|
'<div class="col-sm-12"><div class="alert alert-danger">' +
|
|
error + '</div></div>');
|
|
});
|
|
$fieldset.find('input, select, textarea').first().focus();
|
|
return;
|
|
}
|
|
// Add field errors.
|
|
$field = $fieldset.find('[name="' + field + '"]');
|
|
$field.closest('.form-group').addClass('has-error');
|
|
$.each(errors, function (index, error) {
|
|
$field.closest(".form-control").after(
|
|
'<span class="help-block">' +
|
|
error + '</span>');
|
|
});
|
|
// Focus the first invalid field.
|
|
if (first_field) {
|
|
$field.focus();
|
|
first_field = false;
|
|
}
|
|
});
|
|
});
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
$('.workflow.wizard').bootstrapWizard({
|
|
tabClass: 'wizard-tabs',
|
|
nextSelector: '.button-next',
|
|
previousSelector: '.button-previous',
|
|
onTabShow: function (tab, navigation, index) {
|
|
var $navs = navigation.find('li');
|
|
var total = $navs.length;
|
|
var current = index;
|
|
var $footer = $('.modal-footer');
|
|
_max_visited_step = Math.max(_max_visited_step, current);
|
|
if (current + 1 >= total) {
|
|
$footer.find('.button-next').hide();
|
|
$footer.find('.button-final').show();
|
|
} else {
|
|
$footer.find('.button-next').show();
|
|
$footer.find('.button-final').hide();
|
|
}
|
|
$navs.each(function(i) {
|
|
var $this = $(this);
|
|
if (i <= _max_visited_step) {
|
|
$this.addClass('done');
|
|
} else {
|
|
$this.removeClass('done');
|
|
}
|
|
});
|
|
},
|
|
onNext: function ($tab, $nav, index) {
|
|
return _validate_steps(index - 1);
|
|
},
|
|
onTabClick: function ($tab, $nav, current, index) {
|
|
// Validate if moving forward, but move backwards without validation
|
|
return (index <= current ||
|
|
_validate_steps(current, index - 1) !== false);
|
|
}
|
|
});
|
|
};
|
|
|
|
horizon.modals.getUploadUrl = function(jqXHR) {
|
|
return jqXHR.getResponseHeader("X-File-Upload-URL");
|
|
};
|
|
|
|
horizon.modals.fileUpload = function(url, file, jqXHR) {
|
|
var token = jqXHR.getResponseHeader('X-Auth-Token');
|
|
|
|
horizon.modals.progress_bar(gettext("Uploading image"));
|
|
return $.ajax({
|
|
type: 'PUT',
|
|
url: url,
|
|
xhrFields: {
|
|
withCredentials: true
|
|
},
|
|
headers: {
|
|
'X-Auth-Token': token
|
|
},
|
|
data: file,
|
|
processData: false, // tell jQuery not to process the data
|
|
contentType: 'application/octet-stream',
|
|
xhr: function() {
|
|
var xhr = new window.XMLHttpRequest();
|
|
xhr.upload.addEventListener('progress', function(evt) {
|
|
if (evt.lengthComputable) {
|
|
horizon.modals.progress_bar.update(evt.loaded / evt.total);
|
|
}
|
|
}, false);
|
|
return xhr;
|
|
}
|
|
});
|
|
};
|
|
|
|
horizon.modals.prepareFileUpload = function($form) {
|
|
var $elem = $form.find('input[data-external-upload]');
|
|
if (!$elem.length) {
|
|
return undefined;
|
|
}
|
|
var file = $elem.get(0).files[0];
|
|
var $hiddenPseudoFile = $form.find('input[name="' + $elem.attr('name') + '__hidden"]');
|
|
if (file) {
|
|
$hiddenPseudoFile.val(file.name);
|
|
$elem.remove();
|
|
return file;
|
|
} else {
|
|
$hiddenPseudoFile.val('');
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
horizon.addInitFunction(horizon.modals.init = function() {
|
|
|
|
var $document = $(document);
|
|
|
|
// Bind handler for initializing new modals.
|
|
$('#modal_wrapper').on('new_modal', function (evt, modal) {
|
|
horizon.modals.initModal(modal);
|
|
});
|
|
|
|
// Bind "cancel" button handler.
|
|
$document.on('click', '.modal .cancel', function (evt) {
|
|
$(this).closest('.modal').modal('hide');
|
|
evt.preventDefault();
|
|
});
|
|
|
|
// AJAX form submissions from modals. Makes validation happen in-modal.
|
|
$document.on('submit', '.modal form', function (evt) {
|
|
var $form = $(this),
|
|
form = this,
|
|
$button = $form.find(".modal-footer .btn-primary"),
|
|
update_field_id = $form.attr("data-add-to-field"),
|
|
headers = {},
|
|
modalFileUpload = $form.attr("enctype") === "multipart/form-data",
|
|
formData, ajaxOpts, featureFileList, featureFormData, file;
|
|
|
|
if (modalFileUpload) {
|
|
featureFileList = $("<input type='file'/>").get(0).files !== undefined;
|
|
featureFormData = window.FormData !== undefined;
|
|
|
|
if (!featureFileList || !featureFormData) {
|
|
// Test whether browser supports HTML5 FileList and FormData interfaces,
|
|
// which make XHR file upload possible. If not, it doesn't
|
|
// support setting custom headers in AJAX requests either, so
|
|
// modal forms won't work in them (namely, IE9).
|
|
return;
|
|
} else {
|
|
file = horizon.modals.prepareFileUpload($form);
|
|
formData = new window.FormData(form);
|
|
}
|
|
} else {
|
|
formData = $form.serialize();
|
|
}
|
|
evt.preventDefault();
|
|
|
|
// Prevent duplicate form POSTs
|
|
$button.prop("disabled", true);
|
|
|
|
if (update_field_id) {
|
|
headers["X-Horizon-Add-To-Field"] = update_field_id;
|
|
}
|
|
|
|
function processServerSuccess(data, textStatus, jqXHR) {
|
|
var redirect_header = jqXHR.getResponseHeader("X-Horizon-Location"),
|
|
add_to_field_header = jqXHR.getResponseHeader("X-Horizon-Add-To-Field"),
|
|
json_data, field_to_update;
|
|
if (redirect_header === null) {
|
|
$('.ajax-modal, .dropdown-toggle').prop("disabled", false);
|
|
}
|
|
|
|
content_disposition = jqXHR.getResponseHeader("content-disposition");
|
|
if (content_disposition && content_disposition.lastIndexOf("attachment;") !== -1) {
|
|
// if the response was a file, we download the data
|
|
var file_name_regex = /(?:^|\s)filename=(.*?)(?:\s|$)/g;
|
|
var filename = file_name_regex.exec(content_disposition)[1];
|
|
var modal_attachment_link = document.createElement('a');
|
|
modal_attachment_link.setAttribute("id", "modal_attachment_link");
|
|
modal_attachment_link.setAttribute("style", "display:none");
|
|
var dataStr = "data:text/plain;charset=utf-8," + encodeURIComponent(data);
|
|
modal_attachment_link.setAttribute("href", dataStr);
|
|
modal_attachment_link.setAttribute("download", filename);
|
|
var modal_wrapper = document.getElementById("modal_wrapper");
|
|
modal_wrapper.appendChild(modal_attachment_link);
|
|
modal_attachment_link.click();
|
|
modal_attachment_link.remove();
|
|
|
|
if (redirect_header) {
|
|
location.href = redirect_header;
|
|
}
|
|
} else if (redirect_header) {
|
|
location.href = redirect_header;
|
|
} else if (add_to_field_header) {
|
|
json_data = $.parseJSON(data);
|
|
field_to_update = $("#" + add_to_field_header);
|
|
field_to_update.append("<option value='" + json_data[0] + "'>" + json_data[1] + "</option>");
|
|
field_to_update.change();
|
|
field_to_update.val(json_data[0]);
|
|
} else {
|
|
horizon.modals.success(data, textStatus, jqXHR);
|
|
}
|
|
}
|
|
|
|
function processServerError(jqXHR, textStatus, errorThrown, $formElement) {
|
|
$formElement = $formElement || $form;
|
|
if (jqXHR.getResponseHeader('logout')) {
|
|
location.href = jqXHR.getResponseHeader("X-Horizon-Location");
|
|
} else {
|
|
$('.ajax-modal, .dropdown-toggle').prop("disabled", false);
|
|
$formElement.closest(".modal").modal("hide");
|
|
horizon.toast.add("danger", gettext("There was an error submitting the form. Please try again."));
|
|
}
|
|
}
|
|
|
|
ajaxOpts = {
|
|
type: "POST",
|
|
url: $form.attr('action'),
|
|
headers: headers,
|
|
data: formData,
|
|
beforeSend: function () {
|
|
$("#modal_wrapper .modal").last().modal("hide");
|
|
$('.ajax-modal, .dropdown-toggle').attr('disabled', true);
|
|
horizon.modals.modal_spinner(gettext("Working"));
|
|
},
|
|
complete: function (jqXHR) {
|
|
var redirect_header = jqXHR.getResponseHeader("X-Horizon-Location");
|
|
if (redirect_header === null) {
|
|
horizon.modals.spinner.modal('hide');
|
|
}
|
|
$("#modal_wrapper .modal").last().modal("show");
|
|
$button.prop("disabled", false);
|
|
},
|
|
success: function (data, textStatus, jqXHR) {
|
|
var promise;
|
|
var uploadUrl = horizon.modals.getUploadUrl(jqXHR);
|
|
$form.closest(".modal").modal("hide");
|
|
if (uploadUrl) {
|
|
promise = horizon.modals.fileUpload(uploadUrl, file, jqXHR);
|
|
}
|
|
if (promise) {
|
|
promise.then(function() {
|
|
// ignore data resolved in asyncUpload promise
|
|
processServerSuccess(data, textStatus, jqXHR);
|
|
}, function(jqXHR, statusText, errorThrown) {
|
|
var $progressBar = horizon.modals.bar.find('.progress-bar');
|
|
processServerError(jqXHR, statusText, errorThrown, $progressBar);
|
|
});
|
|
} else {
|
|
processServerSuccess(data, textStatus, jqXHR);
|
|
}
|
|
},
|
|
error: processServerError
|
|
};
|
|
|
|
if (modalFileUpload) {
|
|
ajaxOpts.contentType = false; // tell jQuery not to process the data
|
|
ajaxOpts.processData = false; // tell jQuery not to set contentType
|
|
}
|
|
$.ajax(ajaxOpts);
|
|
});
|
|
|
|
// Position modal so it's in-view even when scrolled down.
|
|
$document.on('show.bs.modal', '.modal', function (evt) {
|
|
// avoid closing the modal when escape is pressed on a select input
|
|
$("select", evt.target).keyup(function (e) {
|
|
if (e.keyCode === 27) {
|
|
// remove the focus on the select, so double escape close the modal
|
|
e.target.blur();
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Focus the first usable form field in the modal for accessibility.
|
|
horizon.modals.addModalInitFunction(function (modal) {
|
|
$(modal).find(":text, select, textarea").filter(":visible:first").focus();
|
|
});
|
|
|
|
horizon.modals.addModalInitFunction(function(modal) {
|
|
horizon.datatables.validate_button($(modal).find(".table_wrapper > form"));
|
|
});
|
|
|
|
// Compile angular directives in the modal DOM
|
|
horizon.modals.addModalInitFunction(horizon.utils.loadAngular);
|
|
|
|
// Load modals for ajax-modal links.
|
|
$document.on('click', '.ajax-modal', function (evt) {
|
|
var $this = $(this);
|
|
|
|
// If there's an existing modal request open, cancel it out.
|
|
if (horizon.modals._request && typeof(horizon.modals._request.abort) !== undefined) {
|
|
horizon.modals._request.abort();
|
|
}
|
|
|
|
horizon.modals._request = $.ajax($this.attr('href'), {
|
|
beforeSend: function () {
|
|
horizon.modals.modal_spinner(gettext("Loading"));
|
|
},
|
|
complete: function () {
|
|
// Clear the global storage;
|
|
horizon.modals._request = null;
|
|
horizon.modals.spinner.modal('hide');
|
|
},
|
|
error: function(jqXHR) {
|
|
if (jqXHR.status === 401){
|
|
var redir_url = jqXHR.getResponseHeader("X-Horizon-Location");
|
|
if (redir_url){
|
|
location.href = redir_url;
|
|
} else {
|
|
location.reload(true);
|
|
}
|
|
}
|
|
else {
|
|
if (!horizon.ajax.get_messages(jqXHR)) {
|
|
// Generic error handler. Really generic.
|
|
horizon.toast.add("danger", gettext("An error occurred. Please try again later."));
|
|
}
|
|
}
|
|
},
|
|
success: function (data, textStatus, jqXHR) {
|
|
var update_field_id = $this.attr('data-add-to-field'),
|
|
modal,
|
|
form;
|
|
modal = horizon.modals.success(data, textStatus, jqXHR);
|
|
if (update_field_id) {
|
|
form = modal.find("form");
|
|
if (form.length) {
|
|
form.attr("data-add-to-field", update_field_id);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
evt.preventDefault();
|
|
});
|
|
|
|
|
|
/* Manage the modal "stack" */
|
|
|
|
// After a modal has been shown, hide any other modals that are already in
|
|
// the stack. Only one modal can be visible at the same time.
|
|
$document.on("show.bs.modal", ".modal", function () {
|
|
var modal_stack = $("#modal_wrapper .modal");
|
|
modal_stack.splice(modal_stack.length - 1, 1);
|
|
modal_stack.modal("hide");
|
|
});
|
|
|
|
// After a modal has been fully hidden, remove it to avoid confusion.
|
|
// Note: the modal should only be removed if it is the "top" of the stack of
|
|
// modals, e.g. it's the one currently being interacted with and isn't just
|
|
// temporarily being hidden.
|
|
$document.on('hidden.bs.modal', '.modal', function () {
|
|
var $this = $(this),
|
|
modal_stack = $("#modal_wrapper .modal");
|
|
if ($this[0] === modal_stack.last()[0] || $this.hasClass("loading")) {
|
|
$this.remove();
|
|
if (!$this.hasClass("loading")) {
|
|
$("#modal_wrapper .modal").last().modal("show");
|
|
}
|
|
}
|
|
});
|
|
|
|
// Make modals draggable
|
|
$document.on("show.bs.modal", ".modal", function () {
|
|
$(".modal-content").draggable({
|
|
handle: ".modal-header"
|
|
});
|
|
});
|
|
|
|
// Helper class to show modal spinner on click
|
|
// In case the event was generated by clicking any mouse button,
|
|
// the normalized codes are matched according to http://api.jquery.com/event.which/
|
|
var MOUSE_LBUTTON_CODE_NORMALIZED = 1;
|
|
var MOUSE_WHEEL_CODE_NORMALIZED = 2;
|
|
$(document).on('click', '.openstack-spin', function(ev) {
|
|
// NOTE(tsufiev): prevent infinite 'Loading' spinner when opening link
|
|
// in the other browser tab with mouse wheel or mouse lbutton + modifier
|
|
if (ev.which !== MOUSE_WHEEL_CODE_NORMALIZED &&
|
|
!(ev.which === MOUSE_LBUTTON_CODE_NORMALIZED &&
|
|
(ev.shiftKey || ev.ctrlKey || ev.metaKey))) {
|
|
horizon.modals.modal_spinner(gettext("Loading"));
|
|
}
|
|
});
|
|
});
|