492 lines
19 KiB
JavaScript
492 lines
19 KiB
JavaScript
$(function() {
|
|
//~~ Lodash setup
|
|
|
|
_.mixin({"sprintf": sprintf, "vsprintf": vsprintf});
|
|
|
|
//~~ Logging setup
|
|
|
|
log.setLevel(CONFIG_DEBUG ? "debug" : "info");
|
|
|
|
//~~ 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
|
|
$.ajaxSetup({
|
|
type: 'POST',
|
|
headers: { "cache-control": "no-cache" }
|
|
});
|
|
|
|
// send the current UI API key with any request
|
|
$.ajaxSetup({
|
|
headers: {"X-Api-Key": UI_API_KEY}
|
|
});
|
|
|
|
//~~ 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;
|
|
|
|
//~~ 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){
|
|
var viewModelClass = viewModel[0];
|
|
var viewModelParameters = viewModel[1];
|
|
|
|
if (viewModelParameters != undefined) {
|
|
if (!_.isArray(viewModelParameters)) {
|
|
viewModelParameters = [viewModelParameters];
|
|
}
|
|
|
|
// now we'll try to resolve all of the view model's constructor parameters via our view model map
|
|
var constructorParameters = _.map(viewModelParameters, function(parameter){
|
|
return viewModelMap[parameter]
|
|
});
|
|
} else {
|
|
constructorParameters = [];
|
|
}
|
|
|
|
if (_.some(constructorParameters, function(parameter) { return parameter === undefined; })) {
|
|
var _extractName = function(entry) { return entry[0]; };
|
|
var _onlyUnresolved = function(entry) { return entry[1] === undefined; };
|
|
var missingParameters = _.map(_.filter(_.zip(viewModelParameters, constructorParameters), _onlyUnresolved), _extractName);
|
|
log.debug("Postponing", viewModel[0].name, "due to missing parameters:", missingParameters);
|
|
return;
|
|
}
|
|
|
|
// if we came this far then we could resolve all constructor parameters, so let's construct that view model
|
|
log.debug("Constructing", viewModel[0].name, "with parameters:", viewModelParameters);
|
|
return new viewModelClass(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(viewModel){
|
|
var name = viewModel[0].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;
|
|
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();
|
|
var viewModelId = _getViewModelId(viewModel);
|
|
|
|
// make sure that we don't have two view models going by the same name
|
|
if (_.has(viewModelMap, viewModelId)) {
|
|
log.error("Duplicate name while instantiating " + viewModelId);
|
|
continue;
|
|
}
|
|
|
|
var viewModelInstance = _createViewModelInstance(viewModel, viewModelMap);
|
|
|
|
// 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[2];
|
|
if (!_.isArray(viewModelBindTargets)) {
|
|
viewModelBindTargets = [viewModelBindTargets];
|
|
}
|
|
|
|
if (additionalBindings.hasOwnProperty(viewModelId)) {
|
|
viewModelBindTargets = viewModelBindTargets.concat(additionalBindings[viewModelId]);
|
|
}
|
|
|
|
allViewModelData.push([viewModelInstance, viewModelBindTargets]);
|
|
allViewModels.push(viewModelInstance);
|
|
viewModelMap[viewModelId] = viewModelInstance;
|
|
}
|
|
|
|
// 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) {
|
|
log.error("Could not instantiate the following view models due to unresolvable dependencies:");
|
|
_.each(unprocessedViewModels, function(entry) {
|
|
log.error(entry[0].name + " (missing: " + _.filter(entry[1], 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");
|
|
|
|
var dataUpdater = new DataUpdater(allViewModels);
|
|
|
|
//~~ Custom knockout.js bindings
|
|
|
|
ko.bindingHandlers.popover = {
|
|
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
|
var val = ko.utils.unwrapObservable(valueAccessor());
|
|
|
|
var options = {
|
|
title: val.title,
|
|
animation: val.animation,
|
|
placement: val.placement,
|
|
trigger: val.trigger,
|
|
delay: val.delay,
|
|
content: val.content,
|
|
html: val.html
|
|
};
|
|
$(element).popover(options);
|
|
}
|
|
};
|
|
|
|
ko.bindingHandlers.allowBindings = {
|
|
init: function (elem, valueAccessor) {
|
|
return { controlsDescendantBindings: !valueAccessor() };
|
|
}
|
|
};
|
|
ko.virtualElements.allowedBindings.allowBindings = true;
|
|
|
|
ko.bindingHandlers.slimScrolledForeach = {
|
|
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
|
|
return ko.bindingHandlers.foreach.init(element, valueAccessor(), allBindings, viewModel, bindingContext);
|
|
},
|
|
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
|
|
setTimeout(function() {
|
|
$(element).slimScroll({scrollBy: 0});
|
|
}, 10);
|
|
return ko.bindingHandlers.foreach.update(element, valueAccessor(), allBindings, viewModel, bindingContext);
|
|
}
|
|
};
|
|
|
|
ko.bindingHandlers.qrcode = {
|
|
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
|
|
var val = ko.utils.unwrapObservable(valueAccessor());
|
|
|
|
var defaultOptions = {
|
|
text: "",
|
|
size: 200,
|
|
fill: "#000",
|
|
background: null,
|
|
label: "",
|
|
fontname: "sans",
|
|
fontcolor: "#000",
|
|
radius: 0,
|
|
ecLevel: "L"
|
|
};
|
|
|
|
var options = {};
|
|
_.each(defaultOptions, function(value, key) {
|
|
options[key] = ko.utils.unwrapObservable(val[key]) || value;
|
|
});
|
|
|
|
$(element).empty().qrcode(options);
|
|
}
|
|
};
|
|
|
|
ko.bindingHandlers.invisible = {
|
|
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
|
|
if (!valueAccessor()) return;
|
|
ko.bindingHandlers.style.update(element, function() {
|
|
return { visibility: 'hidden' };
|
|
})
|
|
}
|
|
};
|
|
|
|
ko.bindingHandlers.contextMenu = {
|
|
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
|
var val = ko.utils.unwrapObservable(valueAccessor());
|
|
|
|
$(element).contextMenu(val);
|
|
},
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
|
var val = ko.utils.unwrapObservable(valueAccessor());
|
|
|
|
$(element).contextMenu(val);
|
|
}
|
|
};
|
|
|
|
//~~ 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: "absolute",
|
|
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 tabs = $('#tabs a[data-toggle="tab"]');
|
|
tabs.on('show', function (e) {
|
|
var current = e.target.hash;
|
|
var previous = e.relatedTarget.hash;
|
|
|
|
_.each(allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onTabChange")) {
|
|
viewModel.onTabChange(current, previous);
|
|
}
|
|
});
|
|
});
|
|
|
|
tabs.on('shown', function (e) {
|
|
var current = e.target.hash;
|
|
var previous = e.relatedTarget.hash;
|
|
|
|
_.each(allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onAfterTabChange")) {
|
|
viewModel.onAfterTabChange(current, previous);
|
|
}
|
|
});
|
|
});
|
|
|
|
// 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();
|
|
});
|
|
|
|
//~~ Starting up the app
|
|
|
|
_.each(allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onStartup")) {
|
|
viewModel.onStartup();
|
|
}
|
|
});
|
|
|
|
//~~ 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];
|
|
}
|
|
|
|
_.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);
|
|
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));
|
|
}
|
|
});
|
|
}
|
|
|
|
if (viewModel.hasOwnProperty("onAfterBinding")) {
|
|
viewModel.onAfterBinding();
|
|
}
|
|
});
|
|
|
|
_.each(allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onAllBound")) {
|
|
viewModel.onAllBound(allViewModels);
|
|
}
|
|
});
|
|
log.info("... binding done");
|
|
|
|
_.each(allViewModels, function(viewModel) {
|
|
if (viewModel.hasOwnProperty("onStartupComplete")) {
|
|
viewModel.onStartupComplete();
|
|
}
|
|
});
|
|
};
|
|
|
|
if (!_.has(viewModelMap, "settingsViewModel")) {
|
|
throw new Error("settingsViewModel is missing, can't run UI")
|
|
}
|
|
viewModelMap["settingsViewModel"].requestData(bindViewModels);
|
|
}
|
|
);
|
|
|