MrDraw/src/octoprint/static/js/app/main.js
Gina Häußge ff595a7e31 Move notifications down a bit
See discussion in #1741
2017-02-22 15:19:04 +01:00

606 lines
25 KiB
JavaScript

$(function() {
OctoPrint = window.OctoPrint;
//~~ Lodash setup
_.mixin({"sprintf": sprintf, "vsprintf": vsprintf});
//~~ Logging setup
log.setLevel(CONFIG_DEBUG ? "debug" : "info");
//~~ OctoPrint client setup
OctoPrint.options.baseurl = BASEURL;
OctoPrint.options.apikey = UI_API_KEY;
var l10n = getQueryParameterByName("l10n");
if (l10n) {
OctoPrint.options.locale = l10n;
}
OctoPrint.socket.onMessage("connected", function(data) {
var payload = data.data;
OctoPrint.options.apikey = payload.apikey;
// update the API key directly in jquery's ajax options too,
// to ensure the fileupload plugin and any plugins still using
// $.ajax directly still work fine too
UI_API_KEY = payload["apikey"];
$.ajaxSetup({
headers: {"X-Api-Key": UI_API_KEY}
});
});
//~~ some CoreUI specific stuff we put into OctoPrint.coreui
OctoPrint.coreui = (function() {
var exports = {
browserTabVisibility: undefined,
selectedTab: undefined,
settingsOpen: false,
wizardOpen: false
};
var browserVisibilityCallbacks = [];
var getHiddenProp = function() {
var prefixes = ["webkit", "moz", "ms", "o"];
// if "hidden" is natively supported just return it
if ("hidden" in document) {
return "hidden"
}
// otherwise loop over all the known prefixes until we find one
var vendorPrefix = _.find(prefixes, function(prefix) {
return (prefix + "Hidden" in document);
});
if (vendorPrefix !== undefined) {
return vendorPrefix + "Hidden";
}
// nothing found
return undefined;
};
var isHidden = function() {
var prop = getHiddenProp();
if (!prop) return false;
return document[prop];
};
var updateBrowserVisibility = function() {
var visible = !isHidden();
exports.browserTabVisible = visible;
_.each(browserVisibilityCallbacks, function(callback) {
callback(visible);
})
};
// register for browser visibility tracking
var prop = getHiddenProp();
if (prop) {
var eventName = prop.replace(/[H|h]idden/, "") + "visibilitychange";
document.addEventListener(eventName, updateBrowserVisibility);
updateBrowserVisibility();
}
// exports
exports.isVisible = function() { return !isHidden() };
exports.onBrowserVisibilityChange = function(callback) {
browserVisibilityCallbacks.push(callback);
};
return exports;
})();
//~~ AJAX setup
// work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at
// http://stackoverflow.com/questions/12506897/is-safari-on-ios-6-caching-ajax-results
$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
if (options.type != "GET") {
var headers;
if (options.hasOwnProperty("headers")) {
options.headers["Cache-Control"] = "no-cache";
} else {
options.headers = { "Cache-Control": "no-cache" };
}
}
});
// send the current UI API key with any request
$.ajaxSetup({
headers: {"X-Api-Key": UI_API_KEY}
});
//~~ Initialize file upload plugin
$.widget("blueimp.fileupload", $.blueimp.fileupload, {
options: {
dropZone: null,
pasteZone: null
}
});
//~~ Initialize i18n
var catalog = window["BABEL_TO_LOAD_" + LOCALE];
if (catalog === undefined) {
catalog = {messages: undefined, plural_expr: undefined, locale: undefined, domain: undefined}
}
babel.Translations.load(catalog).install();
moment.locale(LOCALE);
// Dummy translation requests for dynamic strings supplied by the backend
var dummyTranslations = [
// printer states
gettext("Offline"),
gettext("Opening serial port"),
gettext("Detecting serial port"),
gettext("Detecting baudrate"),
gettext("Connecting"),
gettext("Operational"),
gettext("Printing from SD"),
gettext("Sending file to SD"),
gettext("Printing"),
gettext("Paused"),
gettext("Closed"),
gettext("Transfering file to SD")
];
//~~ Initialize PNotify
PNotify.prototype.options.styling = "bootstrap2";
PNotify.prototype.options.mouse_reset = false;
PNotify.prototype.options.stack.firstpos1 = 40 + 20; // navbar + 20
PNotify.prototype.options.stack.firstpos2 = 20;
PNotify.prototype.options.stack.spacing1 = 20;
PNotify.prototype.options.stack.spacing2 = 20;
PNotify.singleButtonNotify = function(options) {
if (!options.confirm || !options.confirm.buttons || !options.confirm.buttons.length) {
return new PNotify(options);
}
var autoDisplay = options.auto_display != false;
var params = $.extend(true, {}, options);
params.auto_display = false;
var notify = new PNotify(params);
notify.options.confirm.buttons = [notify.options.confirm.buttons[0]];
notify.modules.confirm.makeDialog(notify, notify.options.confirm);
if (autoDisplay) {
notify.open();
}
return notify;
};
//~~ Initialize view models
// the view model map is our basic look up table for dependencies that may be injected into other view models
var viewModelMap = {};
// Fix Function#name on browsers that do not support it (IE):
// see: http://stackoverflow.com/questions/6903762/function-name-not-supported-in-ie
if (!(function f() {}).name) {
Object.defineProperty(Function.prototype, 'name', {
get: function() {
return this.toString().match(/^\s*function\s*(\S*)\s*\(/)[1];
}
});
}
// helper to create a view model instance with injected constructor parameters from the view model map
var _createViewModelInstance = function(viewModel, viewModelMap, optionalDependencyPass) {
// mirror the requested dependencies with an array of the viewModels
var viewModelParametersMap = function(parameter) {
// check if parameter is found within optional array and if all conditions are met return null instead of undefined
if (optionalDependencyPass && viewModel.optional.indexOf(parameter) !== -1 && !viewModelMap[parameter]) {
log.debug("Resolving optional parameter", [parameter], "without viewmodel");
return null; // null == "optional but not available"
}
return viewModelMap[parameter] || undefined; // undefined == "not available"
};
// try to resolve all of the view model's constructor parameters via our view model map
var constructorParameters = _.map(viewModel.dependencies, viewModelParametersMap) || [];
if (constructorParameters.indexOf(undefined) !== -1) {
log.debug("Postponing", viewModel.name, "due to missing parameters:", _.keys(_.pick(_.object(viewModel.dependencies, constructorParameters), _.isUndefined)));
return;
}
// transform array into object if a plugin wants it as an object
constructorParameters = (viewModel.returnObject) ? _.object(viewModel.dependencies, constructorParameters) : constructorParameters;
// if we came this far then we could resolve all constructor parameters, so let's construct that view model
log.debug("Constructing", viewModel.name, "with parameters:", viewModel.dependencies);
return new viewModel.construct(constructorParameters);
};
// map any additional view model bindings we might need to make
var additionalBindings = {};
_.each(OCTOPRINT_ADDITIONAL_BINDINGS, function(bindings) {
var viewModelId = bindings[0];
var viewModelBindTargets = bindings[1];
if (!_.isArray(viewModelBindTargets)) {
viewModelBindTargets = [viewModelBindTargets];
}
if (!additionalBindings.hasOwnProperty(viewModelId)) {
additionalBindings[viewModelId] = viewModelBindTargets;
} else {
additionalBindings[viewModelId] = additionalBindings[viewModelId].concat(viewModelBindTargets);
}
});
// helper for translating the name of a view model class into an identifier for the view model map
var _getViewModelId = function(name){
return name.substr(0, 1).toLowerCase() + name.substr(1); // FooBarViewModel => fooBarViewModel
};
// instantiation loop, will make multiple passes over the list of unprocessed view models until all
// view models have been successfully instantiated with all of their dependencies or no changes can be made
// any more which means not all view models can be instantiated due to missing dependencies
var unprocessedViewModels = OCTOPRINT_VIEWMODELS.slice();
unprocessedViewModels = unprocessedViewModels.concat(ADDITIONAL_VIEWMODELS);
var allViewModels = [];
var allViewModelData = [];
var pass = 1;
var optionalDependencyPass = false;
log.info("Starting dependency resolution...");
while (unprocessedViewModels.length > 0) {
log.debug("Dependency resolution, pass #" + pass);
var startLength = unprocessedViewModels.length;
var postponed = [];
// now try to instantiate every one of our as of yet unprocessed view model descriptors
while (unprocessedViewModels.length > 0){
var viewModel = unprocessedViewModels.shift();
// wrap anything not object related into an object
if(!_.isPlainObject(viewModel)) {
viewModel = {
construct: (_.isArray(viewModel)) ? viewModel[0] : viewModel,
dependencies: viewModel[1] || [],
elements: viewModel[2] || [],
optional: viewModel[3] || []
};
}
// make sure we have atleast a function
if (!_.isFunction(viewModel.construct)) {
log.error("No function to instantiate with", viewModel);
continue;
}
// if name is not set, get name from constructor, if it's an anonymous function generate one
viewModel.name = viewModel.name || _getViewModelId(viewModel.construct.name) || _.uniqueId("unnamedViewModel");
// no alternative names? empty array
viewModel.additionalNames = viewModel.additionalNames || [];
// make sure all value's are in an array
viewModel.dependencies = (_.isArray(viewModel.dependencies)) ? viewModel.dependencies : [viewModel.dependencies];
viewModel.elements = (_.isArray(viewModel.elements)) ? viewModel.elements : [viewModel.elements];
viewModel.optional = (_.isArray(viewModel.optional)) ? viewModel.optional : [viewModel.optional];
viewModel.additionalNames = (_.isArray(viewModel.additionalNames)) ? viewModel.additionalNames : [viewModel.additionalNames];
// make sure that we don't have two view models going by the same name
if (_.has(viewModelMap, viewModel.name)) {
log.error("Duplicate name while instantiating " + viewModel.name);
continue;
}
var viewModelInstance = _createViewModelInstance(viewModel, viewModelMap, optionalDependencyPass);
// our view model couldn't yet be instantiated, so postpone it for a bit
if (viewModelInstance === undefined) {
postponed.push(viewModel);
continue;
}
// we could resolve the depdendencies and the view model is not defined yet => add it, it's now fully processed
var viewModelBindTargets = viewModel.elements;
if (additionalBindings.hasOwnProperty(viewModel.name)) {
viewModelBindTargets = viewModelBindTargets.concat(additionalBindings[viewModel.name]);
}
allViewModelData.push([viewModelInstance, viewModelBindTargets]);
allViewModels.push(viewModelInstance);
viewModelMap[viewModel.name] = viewModelInstance;
if (viewModel.additionalNames.length) {
var registeredAdditionalNames = [];
_.each(viewModel.additionalNames, function(additionalName) {
if (!_.has(viewModelMap, additionalName)) {
viewModelMap[additionalName] = viewModelInstance;
registeredAdditionalNames.push(additionalName);
}
});
if (registeredAdditionalNames.length) {
log.debug("Registered", viewModel.name, "under these additional names:", registeredAdditionalNames);
}
}
}
// anything that's now in the postponed list has to be readded to the unprocessedViewModels
unprocessedViewModels = unprocessedViewModels.concat(postponed);
// if we still have the same amount of items in our list of unprocessed view models it means that we
// couldn't instantiate any more view models over a whole iteration, which in turn mean we can't resolve the
// dependencies of remaining ones, so log that as an error and then quit the loop
if (unprocessedViewModels.length === startLength) {
// I'm gonna let you finish but we will do another pass with the optional dependencies flag enabled
if (!optionalDependencyPass) {
log.debug("Resolving next pass with optional dependencies flag enabled");
optionalDependencyPass = true;
} else {
log.error("Could not instantiate the following view models due to unresolvable dependencies:");
_.each(unprocessedViewModels, function(entry) {
log.error(entry.name + " (missing: " + _.filter(entry.dependencies, function(id) { return !_.has(viewModelMap, id); }).join(", ") + " )");
});
break;
}
}
log.debug("Dependency resolution pass #" + pass + " finished, " + unprocessedViewModels.length + " view models left to process");
pass++;
}
log.info("... dependency resolution done");
//~~ some additional hooks and initializations
// make sure modals max out at the window height
$.fn.modal.defaults.maxHeight = function(){
// subtract the height of the modal header and footer
return $(window).height() - 165;
};
// jquery plugin to select all text in an element
// originally from: http://stackoverflow.com/a/987376
$.fn.selectText = function() {
var doc = document;
var element = this[0];
var range, selection;
if (doc.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText(element);
range.select();
} else if (window.getSelection) {
selection = window.getSelection();
range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
}
};
$.fn.isChildOf = function (element) {
return $(element).has(this).length > 0;
};
// from http://jsfiddle.net/KyleMit/X9tgY/
$.fn.contextMenu = function (settings) {
return this.each(function () {
// Open context menu
$(this).on("contextmenu", function (e) {
// return native menu if pressing control
if (e.ctrlKey) return;
$(settings.menuSelector)
.data("invokedOn", $(e.target))
.data("contextParent", $(this))
.show()
.css({
position: "fixed",
left: getMenuPosition(e.clientX, 'width', 'scrollLeft'),
top: getMenuPosition(e.clientY, 'height', 'scrollTop'),
"z-index": 9999
}).off('click')
.on('click', function (e) {
if (e.target.tagName.toLowerCase() == "input")
return;
$(this).hide();
settings.menuSelected.call(this, $(this).data('invokedOn'), $(this).data('contextParent'), $(e.target));
});
return false;
});
//make sure menu closes on any click
$(document).click(function () {
$(settings.menuSelector).hide();
});
});
function getMenuPosition(mouse, direction, scrollDir) {
var win = $(window)[direction](),
scroll = $(window)[scrollDir](),
menu = $(settings.menuSelector)[direction](),
position = mouse + scroll;
// opening menu would pass the side of the page
if (mouse + menu > win && menu < mouse)
position -= menu;
return position;
}
};
// Use bootstrap tabdrop for tabs and pills
$('.nav-pills, .nav-tabs').tabdrop();
// Allow components to react to tab change
var onTabChange = function(current, previous) {
log.debug("Selected OctoPrint tab changed: previous = " + previous + ", current = " + current);
OctoPrint.coreui.selectedTab = current;
callViewModels(allViewModels, "onTabChange", [current, previous]);
};
var tabs = $('#tabs a[data-toggle="tab"]');
tabs.on('show', function (e) {
var current = e.target.hash;
var previous = e.relatedTarget.hash;
onTabChange(current, previous);
});
tabs.on('shown', function (e) {
var current = e.target.hash;
var previous = e.relatedTarget.hash;
callViewModels(allViewModels, "onAfterTabChange", [current, previous]);
});
onTabChange(OCTOPRINT_INITIAL_TAB);
// Fix input element click problems on dropdowns
$(".dropdown input, .dropdown label").click(function(e) {
e.stopPropagation();
});
// prevent default action for drag-n-drop
$(document).bind("drop dragover", function (e) {
e.preventDefault();
});
// reload overlay
$("#reloadui_overlay_reload").click(function() { location.reload(); });
//~~ view model binding
var bindViewModels = function() {
log.info("Going to bind " + allViewModelData.length + " view models...");
_.each(allViewModelData, function(viewModelData) {
if (!Array.isArray(viewModelData) || viewModelData.length != 2) {
return;
}
var viewModel = viewModelData[0];
var targets = viewModelData[1];
if (targets === undefined) {
return;
}
if (!_.isArray(targets)) {
targets = [targets];
}
if (viewModel.hasOwnProperty("onBeforeBinding")) {
viewModel.onBeforeBinding();
}
if (targets != undefined) {
if (!_.isArray(targets)) {
targets = [targets];
}
viewModel._bindings = [];
_.each(targets, function(target) {
if (target == undefined) {
return;
}
var object;
if (!(target instanceof jQuery)) {
object = $(target);
} else {
object = target;
}
if (object == undefined || !object.length) {
log.info("Did not bind view model", viewModel.constructor.name, "to target", target, "since it does not exist");
return;
}
var element = object.get(0);
if (element == undefined) {
log.info("Did not bind view model", viewModel.constructor.name, "to target", target, "since it does not exist");
return;
}
try {
ko.applyBindings(viewModel, element);
viewModel._bindings.push(target);
if (viewModel.hasOwnProperty("onBoundTo")) {
viewModel.onBoundTo(target, element);
}
log.debug("View model", viewModel.constructor.name, "bound to", target);
} catch (exc) {
log.error("Could not bind view model", viewModel.constructor.name, "to target", target, ":", (exc.stack || exc));
}
});
}
viewModel._unbound = viewModel._bindings != undefined && viewModel._bindings.length == 0;
if (viewModel.hasOwnProperty("onAfterBinding")) {
viewModel.onAfterBinding();
}
});
callViewModels(allViewModels, "onAllBound", [allViewModels]);
log.info("... binding done");
// startup complete
callViewModels(allViewModels, "onStartupComplete");
// make sure we can track the browser tab visibility
OctoPrint.coreui.onBrowserVisibilityChange(function(status) {
log.debug("Browser tab is now " + (status ? "visible" : "hidden"));
callViewModels(allViewModels, "onBrowserTabVisibilityChange", [status]);
});
log.info("Application startup complete");
};
if (!_.has(viewModelMap, "settingsViewModel")) {
throw new Error("settingsViewModel is missing, can't run UI")
}
log.info("Initial application setup done, connecting to server...");
var dataUpdater = new DataUpdater(allViewModels);
dataUpdater.connect()
.done(function() {
log.info("Finalizing application startup");
//~~ Starting up the app
callViewModels(allViewModels, "onStartup");
viewModelMap["settingsViewModel"].requestData()
.done(function() {
// There appears to be an odd race condition either in JQuery's AJAX implementation or
// the browser's implementation of XHR, causing a second GET request from inside the
// completion handler of the very same request to never get its completion handler called
// if ETag headers are present on the response (the status code of the request does NOT
// seem to matter here, only that the ETag header is present).
//
// Minimal example with which I was able to reproduce this behaviour can be found
// at https://gist.github.com/foosel/b2ddb9ebd71b0b63a749444651bfce3f
//
// Decoupling all consecutive calls from this done event handler hence is an easy way
// to avoid this problem. A zero timeout should do the trick nicely.
window.setTimeout(bindViewModels, 0);
});
});
}
);