1172 lines
36 KiB
JavaScript
1172 lines
36 KiB
JavaScript
function ItemListHelper(listType, supportedSorting, supportedFilters, defaultSorting, defaultFilters, exclusiveFilters, filesPerPage) {
|
|
var self = this;
|
|
|
|
self.listType = listType;
|
|
self.supportedSorting = supportedSorting;
|
|
self.supportedFilters = supportedFilters;
|
|
self.defaultSorting = defaultSorting;
|
|
self.defaultFilters = defaultFilters;
|
|
self.exclusiveFilters = exclusiveFilters;
|
|
|
|
self.searchFunction = undefined;
|
|
|
|
self.allItems = [];
|
|
self.allSize = ko.observable(0);
|
|
|
|
self.items = ko.observableArray([]);
|
|
self.pageSize = ko.observable(filesPerPage);
|
|
self.currentPage = ko.observable(0);
|
|
self.currentSorting = ko.observable(self.defaultSorting);
|
|
self.currentFilters = ko.observableArray(self.defaultFilters);
|
|
self.selectedItem = ko.observable(undefined);
|
|
|
|
//~~ item handling
|
|
|
|
self.refresh = function() {
|
|
self._updateItems();
|
|
};
|
|
|
|
self.updateItems = function(items) {
|
|
self.allItems = items;
|
|
self.allSize(items.length);
|
|
self._updateItems();
|
|
};
|
|
|
|
self.selectItem = function(matcher) {
|
|
var itemList = self.items();
|
|
for (var i = 0; i < itemList.length; i++) {
|
|
if (matcher(itemList[i])) {
|
|
self.selectedItem(itemList[i]);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
self.selectNone = function() {
|
|
self.selectedItem(undefined);
|
|
};
|
|
|
|
self.isSelected = function(data) {
|
|
return self.selectedItem() == data;
|
|
};
|
|
|
|
self.isSelectedByMatcher = function(matcher) {
|
|
return matcher(self.selectedItem());
|
|
};
|
|
|
|
self.removeItem = function(matcher) {
|
|
var item = self.getItem(matcher, true);
|
|
if (item === undefined) {
|
|
return;
|
|
}
|
|
|
|
var index = self.allItems.indexOf(item);
|
|
if (index > -1) {
|
|
self.allItems.splice(index, 1);
|
|
self._updateItems();
|
|
}
|
|
};
|
|
|
|
//~~ pagination
|
|
|
|
self.paginatedItems = ko.dependentObservable(function() {
|
|
if (self.items() == undefined) {
|
|
return [];
|
|
} else if (self.pageSize() == 0) {
|
|
return self.items();
|
|
} else {
|
|
var from = Math.max(self.currentPage() * self.pageSize(), 0);
|
|
var to = Math.min(from + self.pageSize(), self.items().length);
|
|
return self.items().slice(from, to);
|
|
}
|
|
});
|
|
self.lastPage = ko.dependentObservable(function() {
|
|
return (self.pageSize() == 0 ? 1 : Math.ceil(self.items().length / self.pageSize()) - 1);
|
|
});
|
|
self.pages = ko.dependentObservable(function() {
|
|
var pages = [];
|
|
if (self.pageSize() == 0) {
|
|
pages.push({ number: 0, text: 1 });
|
|
} else if (self.lastPage() < 7) {
|
|
for (var i = 0; i < self.lastPage() + 1; i++) {
|
|
pages.push({ number: i, text: i+1 });
|
|
}
|
|
} else {
|
|
pages.push({ number: 0, text: 1 });
|
|
if (self.currentPage() < 5) {
|
|
for (var i = 1; i < 5; i++) {
|
|
pages.push({ number: i, text: i+1 });
|
|
}
|
|
pages.push({ number: -1, text: "…"});
|
|
} else if (self.currentPage() > self.lastPage() - 5) {
|
|
pages.push({ number: -1, text: "…"});
|
|
for (var i = self.lastPage() - 4; i < self.lastPage(); i++) {
|
|
pages.push({ number: i, text: i+1 });
|
|
}
|
|
} else {
|
|
pages.push({ number: -1, text: "…"});
|
|
for (var i = self.currentPage() - 1; i <= self.currentPage() + 1; i++) {
|
|
pages.push({ number: i, text: i+1 });
|
|
}
|
|
pages.push({ number: -1, text: "…"});
|
|
}
|
|
pages.push({ number: self.lastPage(), text: self.lastPage() + 1})
|
|
}
|
|
return pages;
|
|
});
|
|
|
|
self.switchToItem = function(matcher) {
|
|
var pos = -1;
|
|
var itemList = self.items();
|
|
for (var i = 0; i < itemList.length; i++) {
|
|
if (matcher(itemList[i])) {
|
|
pos = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (pos > -1) {
|
|
var page = Math.floor(pos / self.pageSize());
|
|
self.changePage(page);
|
|
}
|
|
};
|
|
|
|
self.changePage = function(newPage) {
|
|
if (newPage < 0 || newPage > self.lastPage())
|
|
return;
|
|
self.currentPage(newPage);
|
|
}; self.prevPage = function() {
|
|
if (self.currentPage() > 0) {
|
|
self.currentPage(self.currentPage() - 1);
|
|
}
|
|
};
|
|
self.nextPage = function() {
|
|
if (self.currentPage() < self.lastPage()) {
|
|
self.currentPage(self.currentPage() + 1);
|
|
}
|
|
};
|
|
|
|
self.getItem = function(matcher, all) {
|
|
var itemList;
|
|
if (all !== undefined && all === true) {
|
|
itemList = self.allItems;
|
|
} else {
|
|
itemList = self.items();
|
|
}
|
|
for (var i = 0; i < itemList.length; i++) {
|
|
if (matcher(itemList[i])) {
|
|
return itemList[i];
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
self.resetPage = function() {
|
|
if (self.currentPage() > self.lastPage()) {
|
|
self.currentPage(self.lastPage());
|
|
}
|
|
};
|
|
|
|
//~~ searching
|
|
|
|
self.changeSearchFunction = function(searchFunction) {
|
|
self.searchFunction = searchFunction;
|
|
self.changePage(0);
|
|
self._updateItems();
|
|
};
|
|
|
|
self.resetSearch = function() {
|
|
self.changeSearchFunction(undefined);
|
|
};
|
|
|
|
//~~ sorting
|
|
|
|
self.changeSorting = function(sorting) {
|
|
if (!_.contains(_.keys(self.supportedSorting), sorting))
|
|
return;
|
|
|
|
self.currentSorting(sorting);
|
|
self._saveCurrentSortingToLocalStorage();
|
|
|
|
self.changePage(0);
|
|
self._updateItems();
|
|
};
|
|
|
|
//~~ filtering
|
|
|
|
self.toggleFilter = function(filter) {
|
|
if (!_.contains(_.keys(self.supportedFilters), filter))
|
|
return;
|
|
|
|
if (_.contains(self.currentFilters(), filter)) {
|
|
self.removeFilter(filter);
|
|
} else {
|
|
self.addFilter(filter);
|
|
}
|
|
};
|
|
|
|
self.addFilter = function(filter) {
|
|
if (!_.contains(_.keys(self.supportedFilters), filter))
|
|
return;
|
|
|
|
for (var i = 0; i < self.exclusiveFilters.length; i++) {
|
|
if (_.contains(self.exclusiveFilters[i], filter)) {
|
|
for (var j = 0; j < self.exclusiveFilters[i].length; j++) {
|
|
if (self.exclusiveFilters[i][j] == filter)
|
|
continue;
|
|
self.removeFilter(self.exclusiveFilters[i][j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
var filters = self.currentFilters();
|
|
filters.push(filter);
|
|
self.currentFilters(filters);
|
|
self._saveCurrentFiltersToLocalStorage();
|
|
|
|
self.changePage(0);
|
|
self._updateItems();
|
|
};
|
|
|
|
self.removeFilter = function(filter) {
|
|
if (!_.contains(_.keys(self.supportedFilters), filter))
|
|
return;
|
|
|
|
var filters = self.currentFilters();
|
|
filters = _.without(filters, filter);
|
|
self.currentFilters(filters);
|
|
self._saveCurrentFiltersToLocalStorage();
|
|
|
|
self.changePage(0);
|
|
self._updateItems();
|
|
};
|
|
|
|
//~~ update for sorted and filtered view
|
|
|
|
self._updateItems = function() {
|
|
// determine comparator
|
|
var comparator = undefined;
|
|
var currentSorting = self.currentSorting();
|
|
if (typeof currentSorting !== 'undefined' && typeof self.supportedSorting[currentSorting] !== 'undefined') {
|
|
comparator = self.supportedSorting[currentSorting];
|
|
}
|
|
|
|
// work on all items
|
|
var result = self.allItems;
|
|
|
|
// filter if necessary
|
|
var filters = self.currentFilters();
|
|
_.each(filters, function(filter) {
|
|
if (typeof filter !== 'undefined' && typeof supportedFilters[filter] !== 'undefined')
|
|
result = _.filter(result, supportedFilters[filter]);
|
|
});
|
|
|
|
// search if necessary
|
|
if (typeof self.searchFunction !== 'undefined' && self.searchFunction) {
|
|
result = _.filter(result, self.searchFunction);
|
|
}
|
|
|
|
// sort if necessary
|
|
if (typeof comparator !== 'undefined')
|
|
result.sort(comparator);
|
|
|
|
// set result list
|
|
self.items(result);
|
|
};
|
|
|
|
//~~ local storage
|
|
|
|
self._saveCurrentSortingToLocalStorage = function() {
|
|
if ( self._initializeLocalStorage() ) {
|
|
var currentSorting = self.currentSorting();
|
|
if (currentSorting !== undefined)
|
|
localStorage[self.listType + "." + "currentSorting"] = currentSorting;
|
|
else
|
|
localStorage[self.listType + "." + "currentSorting"] = undefined;
|
|
}
|
|
};
|
|
|
|
self._loadCurrentSortingFromLocalStorage = function() {
|
|
if ( self._initializeLocalStorage() ) {
|
|
if (_.contains(_.keys(supportedSorting), localStorage[self.listType + "." + "currentSorting"]))
|
|
self.currentSorting(localStorage[self.listType + "." + "currentSorting"]);
|
|
else
|
|
self.currentSorting(defaultSorting);
|
|
}
|
|
};
|
|
|
|
self._saveCurrentFiltersToLocalStorage = function() {
|
|
if ( self._initializeLocalStorage() ) {
|
|
var filters = _.intersection(_.keys(self.supportedFilters), self.currentFilters());
|
|
localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(filters);
|
|
}
|
|
};
|
|
|
|
self._loadCurrentFiltersFromLocalStorage = function() {
|
|
if ( self._initializeLocalStorage() ) {
|
|
self.currentFilters(_.intersection(_.keys(self.supportedFilters), JSON.parse(localStorage[self.listType + "." + "currentFilters"])));
|
|
}
|
|
};
|
|
|
|
self._initializeLocalStorage = function() {
|
|
if (!Modernizr.localstorage)
|
|
return false;
|
|
|
|
if (localStorage[self.listType + "." + "currentSorting"] !== undefined && localStorage[self.listType + "." + "currentFilters"] !== undefined && JSON.parse(localStorage[self.listType + "." + "currentFilters"]) instanceof Array)
|
|
return true;
|
|
|
|
localStorage[self.listType + "." + "currentSorting"] = self.defaultSorting;
|
|
localStorage[self.listType + "." + "currentFilters"] = JSON.stringify(self.defaultFilters);
|
|
|
|
return true;
|
|
};
|
|
|
|
self._loadCurrentFiltersFromLocalStorage();
|
|
self._loadCurrentSortingFromLocalStorage();
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (!bytes) return "-";
|
|
|
|
var units = ["bytes", "KB", "MB", "GB"];
|
|
for (var i = 0; i < units.length; i++) {
|
|
if (bytes < 1024) {
|
|
return _.sprintf("%3.1f%s", bytes, units[i]);
|
|
}
|
|
bytes /= 1024;
|
|
}
|
|
return _.sprintf("%.1f%s", bytes, "TB");
|
|
}
|
|
|
|
function bytesFromSize(size) {
|
|
if (size == undefined || size.trim() == "") return undefined;
|
|
|
|
var parsed = size.match(/^([+]?[0-9]*\.?[0-9]+)(?:\s*)?(.*)$/);
|
|
var number = parsed[1];
|
|
var unit = parsed[2].trim();
|
|
|
|
if (unit == "") return parseFloat(number);
|
|
|
|
var units = {
|
|
b: 1,
|
|
byte: 1,
|
|
bytes: 1,
|
|
kb: 1024,
|
|
mb: Math.pow(1024, 2),
|
|
gb: Math.pow(1024, 3),
|
|
tb: Math.pow(1024, 4)
|
|
};
|
|
unit = unit.toLowerCase();
|
|
|
|
if (!units.hasOwnProperty(unit)) {
|
|
return undefined;
|
|
}
|
|
|
|
var factor = units[unit];
|
|
return number * factor;
|
|
}
|
|
|
|
function formatDuration(seconds) {
|
|
if (!seconds) return "-";
|
|
if (seconds < 1) return "00:00:00";
|
|
|
|
var s = seconds % 60;
|
|
var m = (seconds % 3600) / 60;
|
|
var h = seconds / 3600;
|
|
|
|
return _.sprintf(gettext(/* L10N: duration format */ "%(hour)02d:%(minute)02d:%(second)02d"), {hour: h, minute: m, second: s});
|
|
}
|
|
|
|
function formatFuzzyEstimation(seconds, base) {
|
|
if (!seconds || seconds < 1) return "-";
|
|
|
|
var m;
|
|
if (base != undefined) {
|
|
m = moment(base);
|
|
} else {
|
|
m = moment();
|
|
}
|
|
|
|
m.add(seconds, "s");
|
|
return m.fromNow(true);
|
|
}
|
|
|
|
function formatFuzzyPrintTime(totalSeconds) {
|
|
/**
|
|
* Formats a print time estimate in a very fuzzy way.
|
|
*
|
|
* Accuracy decreases the higher the estimation is:
|
|
*
|
|
* * less than 30s: "a few seconds"
|
|
* * 30s to a minute: "less than a minute"
|
|
* * 1 to 30min: rounded to full minutes, above 30s is minute + 1 ("27 minutes", "2 minutes")
|
|
* * 30min to 40min: "40 minutes"
|
|
* * 40min to 50min: "50 minutes"
|
|
* * 50min to 1h: "1 hour"
|
|
* * 1 to 12h: rounded to half hours, 15min to 45min is ".5", above that hour + 1 ("4 hours", "2.5 hours")
|
|
* * 12 to 24h: rounded to full hours, above 30min is hour + 1, over 23.5h is "1 day"
|
|
* * Over a day: rounded to half days, 8h to 16h is ".5", above that days + 1 ("1 day", "4 days", "2.5 days")
|
|
*/
|
|
|
|
if (!totalSeconds || totalSeconds < 1) return "-";
|
|
|
|
var d = moment.duration(totalSeconds, "seconds");
|
|
|
|
var seconds = d.seconds();
|
|
var minutes = d.minutes();
|
|
var hours = d.hours();
|
|
var days = d.days();
|
|
|
|
var replacements = {
|
|
days: days,
|
|
hours: hours,
|
|
minutes: minutes,
|
|
seconds: seconds,
|
|
totalSeconds: totalSeconds
|
|
};
|
|
|
|
var text = "-";
|
|
|
|
if (days >= 1) {
|
|
// days
|
|
if (hours >= 16) {
|
|
replacements.days += 1;
|
|
|
|
if (replacements.days === 1) {
|
|
text = gettext("%(days)d day");
|
|
} else {
|
|
text = gettext("%(days)d days");
|
|
}
|
|
} else if (hours >= 8 && hours < 16) {
|
|
text = gettext("%(days)d.5 days");
|
|
} else {
|
|
if (days === 1) {
|
|
text = gettext("%(days)d day");
|
|
} else {
|
|
text = gettext("%(days)d days");
|
|
}
|
|
}
|
|
} else if (hours >= 1) {
|
|
// only hours
|
|
if (hours < 12) {
|
|
if (minutes < 15) {
|
|
// less than .15 => .0
|
|
if (hours === 1) {
|
|
text = gettext("%(hours)d hour");
|
|
} else {
|
|
text = gettext("%(hours)d hours");
|
|
}
|
|
} else if (minutes >= 15 && minutes < 45) {
|
|
// between .25 and .75 => .5
|
|
text = gettext("%(hours)d.5 hours");
|
|
} else {
|
|
// over .75 => hours + 1
|
|
replacements.hours += 1;
|
|
|
|
if (replacements.hours === 1) {
|
|
text = gettext("%(hours)d hour");
|
|
} else {
|
|
text = gettext("%(hours)d hours");
|
|
}
|
|
}
|
|
} else {
|
|
if (hours === 23 && minutes > 30) {
|
|
// over 23.5 hours => 1 day
|
|
text = gettext("1 day");
|
|
} else {
|
|
if (minutes > 30) {
|
|
// over .5 => hours + 1
|
|
replacements.hours += 1;
|
|
}
|
|
text = gettext("%(hours)d hours");
|
|
}
|
|
}
|
|
} else if (minutes >= 1) {
|
|
// only minutes
|
|
if (minutes < 2) {
|
|
if (seconds < 30) {
|
|
text = gettext("a minute");
|
|
} else {
|
|
text = gettext("2 minutes");
|
|
}
|
|
} else if (minutes < 30) {
|
|
if (seconds > 30) {
|
|
replacements.minutes += 1;
|
|
}
|
|
text = gettext("%(minutes)d minutes");
|
|
} else if (minutes <= 40) {
|
|
text = gettext("40 minutes");
|
|
} else if (minutes <= 50) {
|
|
text = gettext("50 minutes");
|
|
} else {
|
|
text = gettext("1 hour");
|
|
}
|
|
} else {
|
|
// only seconds
|
|
if (seconds < 30) {
|
|
text = gettext("a few seconds");
|
|
} else {
|
|
text = gettext("less than a minute");
|
|
}
|
|
}
|
|
|
|
return _.sprintf(text, replacements);
|
|
}
|
|
|
|
function formatDate(unixTimestamp) {
|
|
if (!unixTimestamp) return "-";
|
|
return moment.unix(unixTimestamp).format(gettext(/* L10N: Date format */ "YYYY-MM-DD HH:mm"));
|
|
}
|
|
|
|
function formatTimeAgo(unixTimestamp) {
|
|
if (!unixTimestamp) return "-";
|
|
return moment.unix(unixTimestamp).fromNow();
|
|
}
|
|
|
|
function formatFilament(filament) {
|
|
if (!filament || !filament["length"]) return "-";
|
|
var result = "%(length).02fm";
|
|
if (filament.hasOwnProperty("volume") && filament.volume) {
|
|
result += " / " + "%(volume).02fcm³";
|
|
}
|
|
return _.sprintf(result, {length: filament["length"] / 1000, volume: filament["volume"]});
|
|
}
|
|
|
|
function cleanTemperature(temp) {
|
|
if (temp === undefined || !_.isNumber(temp)) return "-";
|
|
if (temp < 10) return gettext("off");
|
|
return temp;
|
|
}
|
|
|
|
function formatTemperature(temp, showF) {
|
|
if (temp === undefined || !_.isNumber(temp)) return "-";
|
|
if (temp < 10) return gettext("off");
|
|
if (showF) {
|
|
return _.sprintf("%.1f°C (%.1f°F)", temp, temp * 9 / 5 + 32);
|
|
} else {
|
|
return _.sprintf("%.1f°C", temp);
|
|
}
|
|
}
|
|
|
|
function pnotifyAdditionalInfo(inner) {
|
|
return '<div class="pnotify_additional_info">'
|
|
+ '<div class="pnotify_more"><a href="#" onclick="$(this).children().toggleClass(\'icon-caret-right icon-caret-down\').parent().parent().next().slideToggle(\'fast\')">More <i class="icon-caret-right"></i></a></div>'
|
|
+ '<div class="pnotify_more_container hide">' + inner + '</div>'
|
|
+ '</div>';
|
|
}
|
|
|
|
function ping(url, callback) {
|
|
var img = new Image();
|
|
var calledBack = false;
|
|
|
|
img.onload = function() {
|
|
callback(true);
|
|
calledBack = true;
|
|
};
|
|
img.onerror = function() {
|
|
if (!calledBack) {
|
|
callback(true);
|
|
calledBack = true;
|
|
}
|
|
};
|
|
img.src = url;
|
|
setTimeout(function() {
|
|
if (!calledBack) {
|
|
callback(false);
|
|
calledBack = true;
|
|
}
|
|
}, 1500);
|
|
}
|
|
|
|
function showOfflineOverlay(title, message, reconnectCallback) {
|
|
if (title == undefined) {
|
|
title = gettext("Server is offline");
|
|
}
|
|
|
|
$("#offline_overlay_title").text(title);
|
|
$("#offline_overlay_message").html(message);
|
|
$("#offline_overlay_reconnect").click(reconnectCallback);
|
|
if (!$("#offline_overlay").is(":visible"))
|
|
$("#offline_overlay").show();
|
|
}
|
|
|
|
function hideOfflineOverlay() {
|
|
$("#offline_overlay").hide();
|
|
}
|
|
|
|
function showMessageDialog(msg, options) {
|
|
options = options || {};
|
|
if (_.isPlainObject(msg)) {
|
|
options = msg;
|
|
} else {
|
|
options.message = msg;
|
|
}
|
|
|
|
var title = options.title || "";
|
|
var message = options.message || "";
|
|
var close = options.close || gettext("Close");
|
|
var onclose = options.onclose || undefined;
|
|
var onshow = options.onshow || undefined;
|
|
var onshown = options.onshown || undefined;
|
|
|
|
if (_.isString(message)) {
|
|
message = $("<p>" + message + "</p>");
|
|
}
|
|
|
|
var modalHeader = $('<a href="javascript:void(0)" class="close" data-dismiss="modal" aria-hidden="true">×</a><h3>' + title + '</h3>');
|
|
var modalBody = $(message);
|
|
var modalFooter = $('<a href="javascript:void(0)" class="btn" data-dismiss="modal" aria-hidden="true">' + close + '</a>');
|
|
|
|
var modal = $('<div></div>')
|
|
.addClass('modal hide fade')
|
|
.append($('<div></div>').addClass('modal-header').append(modalHeader))
|
|
.append($('<div></div>').addClass('modal-body').append(modalBody))
|
|
.append($('<div></div>').addClass('modal-footer').append(modalFooter));
|
|
|
|
modal.on("hidden", function() {
|
|
if (onclose && _.isFunction(onclose)) {
|
|
onclose();
|
|
}
|
|
});
|
|
|
|
if (onshow) {
|
|
modal.on("show", onshow);
|
|
}
|
|
|
|
if (onshown) {
|
|
modal.on("shown", onshown);
|
|
}
|
|
|
|
modal.modal("show");
|
|
return modal;
|
|
}
|
|
|
|
function showConfirmationDialog(msg, onacknowledge, options) {
|
|
options = options || {};
|
|
if (_.isPlainObject(msg)) {
|
|
options = msg;
|
|
} else {
|
|
options.message = msg;
|
|
options.onproceed = onacknowledge;
|
|
}
|
|
|
|
var title = options.title || gettext("Are you sure?");
|
|
var message = options.message || "";
|
|
var question = options.question || gettext("Are you sure you want to proceed?");
|
|
var cancel = options.cancel || gettext("Cancel");
|
|
var proceed = options.proceed || gettext("Proceed");
|
|
var proceedClass = options.proceedClass || "danger";
|
|
var onproceed = options.onproceed || undefined;
|
|
var onclose = options.onclose || undefined;
|
|
var dialogClass = options.dialogClass || "";
|
|
|
|
var modalHeader = $('<a href="javascript:void(0)" class="close" data-dismiss="modal" aria-hidden="true">×</a><h3>' + title + '</h3>');
|
|
var modalBody = $('<p>' + message + '</p><p>' + question + '</p>');
|
|
|
|
var cancelButton = $('<a href="javascript:void(0)" class="btn">' + cancel + '</a>')
|
|
.attr("data-dismiss", "modal")
|
|
.attr("aria-hidden", "true");
|
|
var proceedButton = $('<a href="javascript:void(0)" class="btn">' + proceed + '</a>')
|
|
.addClass("btn-" + proceedClass);
|
|
|
|
var modal = $('<div></div>')
|
|
.addClass('modal hide fade')
|
|
.addClass(dialogClass)
|
|
.append($('<div></div>').addClass('modal-header').append(modalHeader))
|
|
.append($('<div></div>').addClass('modal-body').append(modalBody))
|
|
.append($('<div></div>').addClass('modal-footer').append(cancelButton).append(proceedButton));
|
|
modal.on('hidden', function(event) {
|
|
if (onclose && _.isFunction(onclose)) {
|
|
onclose(event);
|
|
}
|
|
});
|
|
modal.modal("show");
|
|
|
|
proceedButton.click(function(e) {
|
|
e.preventDefault();
|
|
if (onproceed && _.isFunction(onproceed)) {
|
|
onproceed(e);
|
|
}
|
|
modal.modal("hide");
|
|
});
|
|
|
|
return modal;
|
|
}
|
|
|
|
/**
|
|
* Shows a progress modal depending on a supplied promise.
|
|
*
|
|
* Will listen to the supplied promise, update the progress on .progress events and
|
|
* enabling the close button and (optionally) closing the dialog on promise resolve.
|
|
*
|
|
* The calling code should call "notify" on the deferred backing the promise and supply:
|
|
*
|
|
* * the text to display on the progress bar and the optional output field and
|
|
* a boolean value indicating whether the operation behind that update was successful or not
|
|
* * a short text to display on the progress bar, a long text to display on the optional output
|
|
* field and a boolean value indicating whether the operation behind that update was
|
|
* successful or not
|
|
*
|
|
* Non-successful progress updates will remove the barClassSuccess class from the progress bar and
|
|
* apply the barClassFailure class and also apply the outputClassFailure to the produced line
|
|
* in the output.
|
|
*
|
|
* To determine the progress, calling code should supply the prognosed maximum number of
|
|
* progress events. An internal counter will increment on each progress event and used together
|
|
* with the max value to calculate the percentage to display on the progress bar.
|
|
*
|
|
* If no max value is set, the progress bar will show a striped animation at 100% fill status
|
|
* to visualize "unknown but ongoing" status.
|
|
*
|
|
* Available options:
|
|
*
|
|
* * title: the title of the modal, defaults to "Progress"
|
|
* * message: the message of the modal, defaults to ""
|
|
* * buttonText: the text on the close button, defaults to "Close"
|
|
* * max: maximum number of expected progress events (when 100% will be reached), defaults
|
|
* to undefined
|
|
* * close: whether to close the dialog on completion, defaults to false
|
|
* * output: whether to display the progress texts in an output field, defaults to false
|
|
* * dialogClass: additional class to apply to the dialog div
|
|
* * barClassSuccess: additional class for the progress bar while all progress events are
|
|
* successful
|
|
* * barClassFailure: additional class for the progress bar when a progress event was
|
|
* unsuccessful
|
|
* * outputClassSuccess: additional class for successful output lines
|
|
* * outputClassFailure: additional class for unsuccessful output lines
|
|
*
|
|
* @param options modal options
|
|
* @param promise promise to monitor
|
|
* @returns {*|jQuery} the modal object
|
|
*/
|
|
function showProgressModal(options, promise) {
|
|
var title = options.title || gettext("Progress");
|
|
var message = options.message || "";
|
|
var buttonText = options.button || gettext("Close");
|
|
var max = options.max || undefined;
|
|
var close = options.close || false;
|
|
var output = options.output || false;
|
|
|
|
var dialogClass = options.dialogClass || "";
|
|
var barClassSuccess = options.barClassSuccess || "";
|
|
var barClassFailure = options.barClassFailure || "bar-danger";
|
|
var outputClassSuccess = options.outputClassSuccess || "";
|
|
var outputClassFailure = options.outputClassFailure || "text-error";
|
|
|
|
var modalHeader = $('<h3>' + title + '</h3>');
|
|
var paragraph = $('<p>' + message + '</p>');
|
|
|
|
var progress = $('<div class="progress progress-text-centered"></div>');
|
|
var progressBar = $('<div class="bar"></div>')
|
|
.addClass(barClassSuccess);
|
|
var progressTextBack = $('<span class="progress-text-back"></span>');
|
|
var progressTextFront = $('<span class="progress-text-front"></span>')
|
|
.width(progress.width());
|
|
|
|
if (max == undefined) {
|
|
progress.addClass("progress-striped active");
|
|
progressBar.width("100%");
|
|
}
|
|
|
|
progressBar
|
|
.append(progressTextFront);
|
|
progress
|
|
.append(progressTextBack)
|
|
.append(progressBar);
|
|
|
|
var button = $('<button class="btn">' + buttonText + '</button>')
|
|
.prop("disabled", true)
|
|
.attr("data-dismiss", "modal")
|
|
.attr("aria-hidden", "true");
|
|
|
|
var modalBody = $('<div></div>')
|
|
.addClass('modal-body')
|
|
.append(paragraph)
|
|
.append(progress);
|
|
|
|
var pre;
|
|
if (output) {
|
|
pre = $("<pre class='pre-scrollable pre-output' style='height: 70px; font-size: 0.8em'></pre>");
|
|
modalBody.append(pre);
|
|
}
|
|
|
|
var modal = $('<div></div>')
|
|
.addClass('modal hide fade')
|
|
.addClass(dialogClass)
|
|
.append($('<div></div>').addClass('modal-header').append(modalHeader))
|
|
.append(modalBody)
|
|
.append($('<div></div>').addClass('modal-footer').append(button));
|
|
modal.modal({keyboard: false, backdrop: "static", show: true});
|
|
|
|
var counter = 0;
|
|
promise
|
|
.progress(function() {
|
|
var short, long, success;
|
|
if (arguments.length === 2) {
|
|
short = long = arguments[0];
|
|
success = arguments[1];
|
|
} else if (arguments.length === 3) {
|
|
short = arguments[0];
|
|
long = arguments[1];
|
|
success = arguments[2];
|
|
} else {
|
|
throw Error("Invalid parameters for showProgressModal, expected either (text, success) or (short, long, success)");
|
|
}
|
|
|
|
var value;
|
|
|
|
if (max === undefined || max <= 0) {
|
|
value = 100;
|
|
} else {
|
|
counter++;
|
|
value = Math.max(Math.min(counter * 100 / max, 100), 0);
|
|
}
|
|
|
|
// update progress bar
|
|
progressBar.width(String(value) + "%");
|
|
progressTextFront.text(short);
|
|
progressTextBack.text(short);
|
|
progressTextFront.width(progress.width());
|
|
|
|
// if not successful, apply failure class
|
|
if (!success && !progressBar.hasClass(barClassFailure)) {
|
|
progressBar
|
|
.removeClass(barClassSuccess)
|
|
.addClass(barClassFailure);
|
|
}
|
|
|
|
if (output && pre) {
|
|
if (success) {
|
|
pre.append($("<span class='" + outputClassSuccess + "'>" + long + "</span>"));
|
|
} else {
|
|
pre.append($("<span class='" + outputClassFailure + "'>" + long + "</span>"));
|
|
}
|
|
pre.scrollTop(pre[0].scrollHeight - pre.height());
|
|
}
|
|
})
|
|
.done(function() {
|
|
button.prop("disabled", false);
|
|
if (close) {
|
|
modal.modal("hide");
|
|
}
|
|
})
|
|
.fail(function() {
|
|
button.prop("disabled", false);
|
|
});
|
|
|
|
return modal;
|
|
}
|
|
|
|
function showReloadOverlay() {
|
|
$("#reloadui_overlay").show();
|
|
}
|
|
|
|
function wrapPromiseWithAlways(p) {
|
|
var deferred = $.Deferred();
|
|
p.always(function() { deferred.resolve.apply(deferred, arguments); });
|
|
return deferred.promise();
|
|
}
|
|
|
|
function commentableLinesToArray(lines) {
|
|
return splitTextToArray(lines, "\n", true, function(item) {return !_.startsWith(item, "#")});
|
|
}
|
|
|
|
function splitTextToArray(text, sep, stripEmpty, filter) {
|
|
return _.filter(
|
|
_.map(
|
|
text.split(sep),
|
|
function(item) { return (item) ? item.trim() : ""; }
|
|
),
|
|
function(item) { return (stripEmpty ? item : true) && (filter ? filter(item) : true); }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns true if comparing data and oldData yields changes, false otherwise.
|
|
*
|
|
* E.g.
|
|
*
|
|
* hasDataChanged(
|
|
* {foo: "bar", fnord: {one: "1", two: "2", three: "three", key: "value"}},
|
|
* {foo: "bar", fnord: {one: "1", two: "2", three: "3", four: "4"}}
|
|
* )
|
|
*
|
|
* will return
|
|
*
|
|
* true
|
|
*
|
|
* and
|
|
*
|
|
* hasDataChanged(
|
|
* {foo: "bar", fnord: {one: "1", two: "2", three: "3"}},
|
|
* {foo: "bar", fnord: {one: "1", two: "2", three: "3"}}
|
|
* )
|
|
*
|
|
* will return
|
|
*
|
|
* false
|
|
*
|
|
* Note that this will assume data and oldData to be structurally identical (same keys)
|
|
* and is optimized to check for value changes, not key updates.
|
|
*/
|
|
function hasDataChanged(data, oldData) {
|
|
if (data == undefined) {
|
|
return false;
|
|
}
|
|
|
|
if (oldData == undefined) {
|
|
return true;
|
|
}
|
|
|
|
if (_.isPlainObject(data)) {
|
|
return _.any(_.keys(data), function(key) {return hasDataChanged(data[key], oldData[key]);});
|
|
} else {
|
|
return !_.isEqual(data, oldData);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare provided data and oldData plain objects and only return those
|
|
* substructures of data that actually changed.
|
|
*
|
|
* E.g.
|
|
*
|
|
* getOnlyChangedData(
|
|
* {foo: "bar", fnord: {one: "1", two: "2", three: "three"}},
|
|
* {foo: "bar", fnord: {one: "1", two: "2", three: "3"}}
|
|
* )
|
|
*
|
|
* will return
|
|
*
|
|
* {fnord: {three: "three"}}
|
|
*
|
|
* and
|
|
*
|
|
* getOnlyChangedData(
|
|
* {foo: "bar", fnord: {one: "1", two: "2", three: "3"}},
|
|
* {foo: "bar", fnord: {one: "1", two: "2", three: "3"}}
|
|
* )
|
|
*
|
|
* will return
|
|
*
|
|
* {}
|
|
*
|
|
* Note that this will assume data and oldData to be structurally identical (same keys)
|
|
* and is optimized to check for value changes, not key updates.
|
|
*/
|
|
function getOnlyChangedData(data, oldData) {
|
|
if (data == undefined) {
|
|
return {};
|
|
}
|
|
|
|
if (oldData == undefined) {
|
|
return data;
|
|
}
|
|
|
|
var f = function(root, oldRoot) {
|
|
if (!_.isPlainObject(root)) {
|
|
return root;
|
|
}
|
|
|
|
var retval = {};
|
|
_.forOwn(root, function(value, key) {
|
|
var oldValue = undefined;
|
|
if (oldRoot != undefined && oldRoot.hasOwnProperty(key)) {
|
|
oldValue = oldRoot[key];
|
|
}
|
|
if (_.isPlainObject(value)) {
|
|
if (oldValue == undefined) {
|
|
retval[key] = value;
|
|
} else if (hasDataChanged(value, oldValue)) {
|
|
retval[key] = f(value, oldValue);
|
|
}
|
|
} else {
|
|
if (!_.isEqual(value, oldValue)) {
|
|
retval[key] = value;
|
|
}
|
|
}
|
|
});
|
|
return retval;
|
|
};
|
|
|
|
return f(data, oldData);
|
|
}
|
|
|
|
function setOnViewModels(allViewModels, key, value) {
|
|
setOnViewModelsIf(allViewModels, key, value, undefined);
|
|
}
|
|
|
|
function setOnViewModelsIf(allViewModels, key, value, condition) {
|
|
if (!allViewModels) return;
|
|
_.each(allViewModels, function(viewModel) {
|
|
setOnViewModelIf(viewModel, key, value, condition);
|
|
})
|
|
}
|
|
|
|
function setOnViewModel(viewModel, key, value) {
|
|
setOnViewModelIf(viewModel, key, value, undefined);
|
|
}
|
|
|
|
function setOnViewModelIf(viewModel, key, value, condition) {
|
|
if (condition === undefined || !_.isFunction(condition)) {
|
|
condition = function() { return true; };
|
|
}
|
|
|
|
try {
|
|
if (!condition(viewModel)) {
|
|
return;
|
|
}
|
|
|
|
viewModel[key] = value;
|
|
} catch (exc) {
|
|
log.error("Error while setting", key, "to", value, "on view model", viewModel.constructor.name, ":", (exc.stack || exc));
|
|
}
|
|
}
|
|
|
|
function callViewModels(allViewModels, method, callback) {
|
|
callViewModelsIf(allViewModels, method, undefined, callback);
|
|
}
|
|
|
|
function callViewModelsIf(allViewModels, method, condition, callback) {
|
|
if (!allViewModels) return;
|
|
|
|
_.each(allViewModels, function(viewModel) {
|
|
try {
|
|
callViewModelIf(viewModel, method, condition, callback);
|
|
} catch (exc) {
|
|
log.error("Error calling", method, "on view model", viewModel.constructor.name, ":", (exc.stack || exc));
|
|
}
|
|
});
|
|
}
|
|
|
|
function callViewModel(viewModel, method, callback, raiseErrors) {
|
|
callViewModelIf(viewModel, method, undefined, callback, raiseErrors);
|
|
}
|
|
|
|
function callViewModelIf(viewModel, method, condition, callback, raiseErrors) {
|
|
raiseErrors = raiseErrors === true || false;
|
|
|
|
if (condition === undefined || !_.isFunction(condition)) {
|
|
condition = function() { return true; };
|
|
}
|
|
|
|
if (!viewModel.hasOwnProperty(method) || !condition(viewModel, method)) return;
|
|
|
|
var parameters = undefined;
|
|
if (!_.isFunction(callback)) {
|
|
// if callback is not a function that means we are supposed to directly
|
|
// call the view model method instead of providing it to the callback
|
|
// - let's figure out how
|
|
|
|
if (callback === undefined) {
|
|
// directly call view model method with no parameters
|
|
parameters = undefined;
|
|
log.trace("Calling method", method, "on view model");
|
|
} else if (_.isArray(callback)) {
|
|
// directly call view model method with these parameters
|
|
parameters = callback;
|
|
log.trace("Calling method", method, "on view model with specified parameters", parameters);
|
|
} else {
|
|
// ok, this doesn't make sense, callback is neither undefined nor
|
|
// an array, we'll return without doing anything
|
|
return;
|
|
}
|
|
|
|
// we reset this here so we now further down that we want to call
|
|
// the method directly
|
|
callback = undefined;
|
|
} else {
|
|
log.trace("Providing method", method, "on view model to specified callback", callback);
|
|
}
|
|
|
|
try {
|
|
if (callback === undefined) {
|
|
if (parameters !== undefined) {
|
|
// call the method with the provided parameters
|
|
viewModel[method].apply(viewModel, parameters);
|
|
} else {
|
|
// call the method without parameters
|
|
viewModel[method]();
|
|
}
|
|
} else {
|
|
// provide the method to the callback
|
|
callback(viewModel[method], viewModel);
|
|
}
|
|
} catch (exc) {
|
|
if (raiseErrors) {
|
|
throw exc;
|
|
} else {
|
|
log.error("Error calling", method, "on view model", viewModel.constructor.name, ":", (exc.stack || exc));
|
|
}
|
|
}
|
|
}
|
|
|
|
var sizeObservable = function(observable) {
|
|
return ko.computed({
|
|
read: function() {
|
|
return formatSize(observable());
|
|
},
|
|
write: function(value) {
|
|
var result = bytesFromSize(value);
|
|
if (result != undefined) {
|
|
observable(result);
|
|
}
|
|
}
|
|
})
|
|
};
|
|
|
|
var getQueryParameterByName = function(name, url) {
|
|
// from http://stackoverflow.com/a/901144/2028598
|
|
if (!url) {
|
|
url = window.location.href;
|
|
}
|
|
name = name.replace(/[\[\]]/g, "\\$&");
|
|
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
|
|
results = regex.exec(url);
|
|
if (!results) return null;
|
|
if (!results[2]) return '';
|
|
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
|
};
|
|
|
|
/**
|
|
* Escapes unprintable ASCII characters in the provided string.
|
|
*
|
|
* E.g. turns a null byte in the string into "\x00".
|
|
*
|
|
* Characters 0 to 31 excluding 9, 10 and 13 will be escaped, as will
|
|
* 127 and 255. That should leave printable characters and unicode
|
|
* alone.
|
|
*
|
|
* Originally based on
|
|
* https://gist.github.com/mathiasbynens/1243213#gistcomment-53590
|
|
*
|
|
* @param str The string to escape
|
|
* @returns {string}
|
|
*/
|
|
var escapeUnprintableCharacters = function(str) {
|
|
var result = "";
|
|
var index = 0;
|
|
var charCode;
|
|
|
|
while (!isNaN(charCode = str.charCodeAt(index))) {
|
|
if ((charCode < 32 && charCode != 9 && charCode != 10 && charCode != 13) || charCode == 127 || charCode == 255) {
|
|
// special hex chars
|
|
result += "\\x" + (charCode > 15 ? "" : "0") + charCode.toString(16)
|
|
} else {
|
|
// anything else
|
|
result += str[index];
|
|
}
|
|
|
|
index++;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
var copyToClipboard = function(text) {
|
|
var temp = $("<textarea>");
|
|
$("body").append(temp);
|
|
temp.val(text).select();
|
|
document.execCommand("copy");
|
|
temp.remove();
|
|
};
|