Browse Source

UiConfig plugin

shortcutme 5 years ago
parent
commit
10bab2b6e4

+ 53 - 0
plugins/UiConfig/UiConfigPlugin.py

@@ -0,0 +1,53 @@
+from Plugin import PluginManager
+from Config import config
+
+
+@PluginManager.afterLoad
+def importPluginnedClasses():
+    from Ui import UiWebsocket
+    UiWebsocket.admin_commands.add("configList")
+
+
+@PluginManager.registerTo("UiRequest")
+class UiRequestPlugin(object):
+    def actionWrapper(self, path, extra_headers=None):
+        if path.strip("/") != "Config":
+            return super(UiRequestPlugin, self).actionWrapper(path, extra_headers)
+
+        if not extra_headers:
+            extra_headers = {}
+        self.sendHeader(extra_headers=extra_headers)
+        site = self.server.site_manager.get(config.homepage)
+        return iter([super(UiRequestPlugin, self).renderWrapper(
+            site, path, "uimedia/plugins/uiconfig/config.html",
+            "Config", extra_headers, show_loadingscreen=False
+        )])
+
+    def actionUiMedia(self, path, *args, **kwargs):
+        if path.startswith("/uimedia/plugins/uiconfig/"):
+            file_path = path.replace("/uimedia/plugins/uiconfig/", "plugins/UiConfig/media/")
+            if config.debug and (file_path.endswith("all.js") or file_path.endswith("all.css")):
+                # If debugging merge *.css to all.css and *.js to all.js
+                from Debug import DebugMedia
+                DebugMedia.merge(file_path)
+            return self.actionFile(file_path)
+        else:
+            return super(UiRequestPlugin, self).actionUiMedia(path)
+
+
+@PluginManager.registerTo("UiWebsocket")
+class UiWebsocketPlugin(object):
+    def actionConfigList(self, to):
+        back = {}
+        config_values = vars(config.arguments)
+        config_values.update(config.pending_changes)
+        for key, val in config_values.iteritems():
+            if key not in config.keys_api_change_allowed:
+                continue
+            back[key] = {
+                "value": val,
+                "default": config.parser.get_default(key),
+                "pending": key in config.pending_changes
+            }
+        return back
+

+ 1 - 0
plugins/UiConfig/__init__.py

@@ -0,0 +1 @@
+import UiConfigPlugin

+ 20 - 0
plugins/UiConfig/media/config.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title>Settings - ZeroNet</title>
+ <meta charset="utf-8" />
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <link rel="stylesheet" href="css/all.css?rev={rev}" />
+</head>
+
+
+<h1>ZeroNet config</h1>
+
+<div class="content" id="content"></div>
+<div class="bottom" id="bottom-save"></div>
+<div class="bottom" id="bottom-restart"></div>
+
+<script type="text/javascript" src="js/all.js"></script>
+</body>
+</html>

+ 63 - 0
plugins/UiConfig/media/css/Config.css

@@ -0,0 +1,63 @@
+body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; backface-visibility: hidden; }
+h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px }
+h2 { margin-top: 10px; }
+h3 { font-weight: normal }
+h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
+a { color: #9760F9 }
+a:hover { text-decoration: none }
+
+.link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s }
+.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; transition: none }
+
+.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; box-sizing: border-box; }
+.section { margin: 0px 10%; }
+.config-items { font-size: 19px; margin-top: 25px; margin-bottom: 75px; }
+.config-item { position: relative; margin: 35px 0px; }
+.config-item .title { display: inline-block; line-height: 36px; }
+.config-item .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; }
+.config-item .description { font-size: 14px; color: #666; line-height: 24px; }
+.config-item .value { display: inline-block;  white-space: nowrap; }
+.config-item .value-right { right: 0px; position: absolute; }
+.config-item .value-fullwidth { width: 100% }
+.config-item .marker {
+	font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px;
+	opacity: 0; pointer-events: none; transition: all 0.6s; transform: scale(2); color: #9760F9;
+}
+.config-item .marker.visible { opacity: 1; pointer-events: all; transform: scale(1); }
+.config-item .marker.changed { color: #2ecc71; }
+.config-item .marker.pending { color: #ffa200; }
+
+
+.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; border-radius: 3px; font-size: 17px; box-sizing: border-box; }
+.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; }
+.input-textarea {  overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; }
+
+.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; }
+
+.value-right .input-text { text-align: right; width: 100px; }
+.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; }
+.value-fullwidth { margin-top: 10px; }
+
+/* Checkbox */
+.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; }
+.checkbox-skin:before {
+	content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px;
+	transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86);
+}
+.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; }
+.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px }
+.checkbox.checked .checkbox-skin:before { margin-left: 27px; }
+.checkbox.checked .checkbox-skin { background-color: #2ECC71 }
+
+/* Bottom */
+
+.bottom {
+	width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px;
+	transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1);; position: fixed; backface-visibility: hidden; box-sizing: border-box;
+}
+.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; }
+.bottom .button { float: right; }
+.bottom.visible { bottom: 0px; box-shadow: 0px 0px 35px #dcdcdc; }
+.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; }
+.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; }
+.bottom-restart .title:before { color: #ffa200; }

File diff suppressed because it is too large
+ 100 - 0
plugins/UiConfig/media/css/all.css


+ 12 - 0
plugins/UiConfig/media/css/button.css

@@ -0,0 +1,12 @@
+/* Button */
+.button {
+	background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center;
+	border-radius: 2px; border-bottom: 2px solid #E8BE29; transition: all 0.5s ease-out; text-decoration: none;
+}
+.button:hover { border-color: white; border-bottom: 2px solid #BD960C; transition: none ; background-color: #FDEB07 }
+.button:active { position: relative; top: 1px }
+.button.loading {
+	color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center;
+	transition: all 0.5s ease-out  ; pointer-events: none; border-bottom: 2px solid #666
+}
+.button.disabled { color: #DDD; background-color: #999;  pointer-events: none; border-bottom: 2px solid #666 }

File diff suppressed because it is too large
+ 10 - 0
plugins/UiConfig/media/css/fonts.css


BIN
plugins/UiConfig/media/img/loading.gif


+ 105 - 0
plugins/UiConfig/media/js/ConfigStorage.coffee

@@ -0,0 +1,105 @@
+class ConfigStorage extends Class
+	constructor: (@config) ->
+		@items = []
+		@createSections()
+		@setValues(@config)
+
+	setValues: (values) ->
+		for section in @items
+			for item in section.items
+				if not values[item.key]
+					continue
+				item.value = @formatValue(values[item.key].value)
+				item.default = @formatValue(values[item.key].default)
+				item.pending = values[item.key].pending
+
+	formatValue: (value) ->
+		if not value
+			return false
+		else if typeof(value) == "object"
+			return value.join("\n")
+		else if typeof(value) == "number"
+			return value.toString()
+		else
+			return value
+
+	deformatValue: (value, type) ->
+		if type == "object"
+			return value.split("\n")
+		if type == "boolean" and not value
+			return false
+		else
+			return value
+
+	createSections: ->
+		section = @createSection("Web Interface")
+
+		# Web Interface
+		section.items.push
+			key: "open_browser"
+			title: "Open web browser on ZeroNet startup"
+			type: "checkbox"
+
+		# Network
+		section = @createSection("Network")
+
+		section.items.push
+			key: "fileserver_port"
+			title: "File server port"
+			type: "text"
+			restrict: "number"
+			description: "Other peers will use this port to reach your served sites. (default: 15441)"
+
+		section.items.push
+			title: "Tor"
+			key: "tor"
+			type: "select"
+			options: [
+				{title: "Disable", value: "disable"}
+				{title: "Enable", value: "enable"}
+				{title: "Always", value: "always"}
+			]
+			value: "Enable"
+			description: [
+				"Disable: Don't connect to peers on Tor network", h("br"),
+				"Enable: Only use Tor for Tor network peers", h("br"),
+				"Always: Use Tor for every connections to hide your IP address (slower)"
+			]
+
+		section.items.push
+			title: "Use Tor bridges"
+			key: "tor_use_bridges"
+			type: "checkbox"
+			description: "Use obfuscated bridge relays to avoid network level Tor block (even slower)"
+
+		section.items.push
+			title: "Trackers"
+			key: "trackers"
+			type: "textarea"
+			description: "Discover new peers using these adresses"
+
+		section.items.push
+			title: "Trackers files"
+			key: "trackers_file"
+			type: "text"
+			description: "Load additional list of torrent trackers dynamically, from a file"
+			placeholder: "Eg.: data/trackers.json"
+			value_pos: "fullwidth"
+
+		section.items.push
+			title: "Proxy for tracker connections"
+			key: "trackers_proxy"
+			type: "select"
+			options: [
+				{title: "Disable", value: "disable"}
+				{title: "Tor", value: "tor"}
+			]
+
+	createSection: (title) =>
+		section = {}
+		section.title = title
+		section.items = []
+		@items.push(section)
+		return section
+
+window.ConfigStorage = ConfigStorage

+ 223 - 0
plugins/UiConfig/media/js/UiConfig.coffee

@@ -0,0 +1,223 @@
+window.h = maquette.h
+
+class UiConfig extends ZeroFrame
+	init: ->
+		@save_visible = true
+		@config = null  # Setting currently set on the server
+		@values = null  # Entered values on the page
+		window.onbeforeunload = =>
+			if @getValuesChanged().length > 0
+				return true
+			else
+				return null
+
+	onOpenWebsocket: =>
+		@cmd("wrapperSetTitle", "Config - ZeroNet")
+		@updateConfig()
+
+	updateConfig: (cb) =>
+		@cmd "configList", [], (res) =>
+			@config = res
+			@values = {}
+			@config_storage = new ConfigStorage(@config)
+			for key, item of res
+				@values[key] = @config_storage.formatValue(item.value)
+			@projector.scheduleRender()
+			cb?()
+
+	createProjector: =>
+		@projector = maquette.createProjector()
+		@projector.replace($("#content"), @render)
+		@projector.replace($("#bottom-save"), @renderBottomSave)
+		@projector.replace($("#bottom-restart"), @renderBottomRestart)
+
+	getValuesChanged: =>
+		values_changed = []
+		for key, value of @values
+			if @config_storage.formatValue(value) != @config_storage.formatValue(@config[key].value)
+				values_changed.push({key: key, value: value})
+		return values_changed
+
+	getValuesPending: =>
+		values_pending = []
+		for key, item of @config
+			if item.pending
+				values_pending.push(key)
+		return values_pending
+
+	saveValues: (cb) =>
+		changed_values = @getValuesChanged()
+		for item, i in changed_values
+			last = i == changed_values.length - 1
+			value = @config_storage.deformatValue(item.value, typeof(@config[item.key].default))
+			value_same_as_default = JSON.stringify(@config[item.key].default) == JSON.stringify(value)
+			if value_same_as_default
+				value = null
+			@saveValue(item.key, value, if last then cb else null)
+
+	saveValue: (key, value, cb) =>
+		if key == "open_browser"
+			if value
+				value = "default_browser"
+			else
+				value = "False"
+
+		Page.cmd "configSet", [key, value], (res) =>
+			if res != "ok"
+				Page.cmd "wrapperNotification", ["error", res.error]
+			cb?()
+
+	renderSection: (section) =>
+		h("div.section", {key: section.title}, [
+			h("h2", section.title),
+			h("div.config-items", section.items.map @renderSectionItem)
+		])
+
+	handleResetClick: (e) =>
+		node = e.currentTarget
+		config_key = node.attributes.config_key.value
+		default_value = node.attributes.default_value?.value
+		Page.cmd "wrapperConfirm", ["Reset #{config_key} value?", "Reset to default"], (res) =>
+			if (res)
+				@values[config_key] = default_value
+			Page.projector.scheduleRender()
+
+	renderSectionItem: (item) =>
+		value_pos = item.value_pos
+
+		if item.type == "textarea"
+			value_pos ?= "fullwidth"
+		else
+			value_pos ?= "right"
+
+		value_changed = @config_storage.formatValue(@values[item.key]) != item.value
+		value_default = @config_storage.formatValue(@values[item.key]) == item.default
+
+		if item.key in ["open_browser", "fileserver_port"]  # Value default for some settings makes no sense
+			value_default = true
+
+		marker_title = "Changed from default value: #{item.default} -> #{@values[item.key]}"
+		if item.pending
+			marker_title += " (change pending until client restart)"
+
+		h("div.config-item", [
+			h("div.title", [
+				h("h3", item.title),
+				h("div.description", item.description)
+			])
+			h("div.value.value-#{value_pos}",
+				if item.type == "select"
+					@renderValueSelect(item)
+				else if item.type == "checkbox"
+					@renderValueCheckbox(item)
+				else if item.type == "textarea"
+					@renderValueTextarea(item)
+				else
+					@renderValueText(item)
+				h("a.marker", {
+					href: "#Reset", title: marker_title,
+					onclick: @handleResetClick, config_key: item.key, default_value: item.default,
+					classes: {default: value_default, changed: value_changed, visible: not value_default or value_changed or item.pending, pending: item.pending}
+				}, "\u2022")
+			)
+		])
+
+	# Values
+	handleInputChange: (e) =>
+		node = e.target
+		config_key = node.attributes.config_key.value
+		@values[config_key] = node.value
+		Page.projector.scheduleRender()
+
+	handleCheckboxChange: (e) =>
+		node = e.currentTarget
+		config_key = node.attributes.config_key.value
+		value = not node.classList.contains("checked")
+		@values[config_key] = value
+		Page.projector.scheduleRender()
+
+	renderValueText: (item) =>
+		value = @values[item.key]
+		if not value
+			value = ""
+		h("input.input-#{item.type}", {type: item.type, config_key: item.key, value: value, placeholder: item.placeholder, oninput: @handleInputChange})
+
+	autosizeTextarea: (e) =>
+		@log "autosize", arguments
+		if e.currentTarget
+			# @handleInputChange(e)
+			node = e.currentTarget
+		else
+			node = e
+		height_before = node.style.height
+		if height_before
+			node.style.height = "0px"
+		h = node.offsetHeight
+		scrollh = node.scrollHeight + 20
+		if scrollh > h
+			node.style.height = scrollh + "px"
+		else
+			node.style.height = height_before
+
+	renderValueTextarea: (item) =>
+		value = @values[item.key]
+		if not value
+			value = ""
+		h("textarea.input-#{item.type}.input-text",{
+			type: item.type, config_key: item.key, oninput: @handleInputChange, afterCreate: @autosizeTextarea, updateAnimation: @autosizeTextarea, value: value
+		})
+
+	renderValueCheckbox: (item) =>
+		if @values[item.key] and @values[item.key] != "False"
+			checked = true
+		else
+			checked = false
+		h("div.checkbox", {onclick: @handleCheckboxChange, config_key: item.key, classes: {checked: checked}}, h("div.checkbox-skin"))
+
+	renderValueSelect: (item) =>
+		h("select.input-select", {config_key: item.key, oninput: @handleInputChange},
+			item.options.map (option) =>
+				h("option", {selected: option.value == @values[item.key], value: option.value}, option.title)
+		)
+
+	render: =>
+		if not @config
+			return h("div.content")
+
+		h("div.content", [
+			@config_storage.items.map @renderSection
+		])
+
+	handleSaveClick: =>
+		@save_loading = true
+		@logStart "Save"
+		@saveValues =>
+			@save_loading = false
+			@logEnd "Save"
+			@updateConfig()
+			Page.projector.scheduleRender()
+		return false
+
+	renderBottomSave: =>
+		values_changed = @getValuesChanged()
+		h("div.bottom.bottom-save", {classes: {visible: values_changed.length}}, h("div.bottom-content", [
+			h("div.title", "#{values_changed.length} configuration item value changed"),
+			h("a.button.button-submit.button-save", {href: "#Save", classes: {loading: @save_loading}, onclick: @handleSaveClick}, "Save settings")
+		]))
+
+	handleRestartClick: =>
+		@restart_loading = true
+		Page.cmd("serverShutdown", {restart: true})
+		Page.projector.scheduleRender()
+		return false
+
+	renderBottomRestart: =>
+		values_pending = @getValuesPending()
+		values_changed = @getValuesChanged()
+		h("div.bottom.bottom-restart", {classes: {visible: values_pending.length and not values_changed.length}}, h("div.bottom-content", [
+			h("div.title", "Some changes settings requires restart"),
+			h("a.button.button-submit.button-restart", {href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick}, "Restart ZeroNet client")
+		]))
+
+window.Page = new UiConfig()
+window.Page.createProjector()

+ 1650 - 0
plugins/UiConfig/media/js/all.js

@@ -0,0 +1,1650 @@
+
+
+/* ---- plugins/UiConfig/media/js/lib/Class.coffee ---- */
+
+
+(function() {
+  var Class,
+    slice = [].slice;
+
+  Class = (function() {
+    function Class() {}
+
+    Class.prototype.trace = true;
+
+    Class.prototype.log = function() {
+      var args;
+      args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+      if (!this.trace) {
+        return;
+      }
+      if (typeof console === 'undefined') {
+        return;
+      }
+      args.unshift("[" + this.constructor.name + "]");
+      console.log.apply(console, args);
+      return this;
+    };
+
+    Class.prototype.logStart = function() {
+      var args, name;
+      name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
+      if (!this.trace) {
+        return;
+      }
+      this.logtimers || (this.logtimers = {});
+      this.logtimers[name] = +(new Date);
+      if (args.length > 0) {
+        this.log.apply(this, ["" + name].concat(slice.call(args), ["(started)"]));
+      }
+      return this;
+    };
+
+    Class.prototype.logEnd = function() {
+      var args, ms, name;
+      name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
+      ms = +(new Date) - this.logtimers[name];
+      this.log.apply(this, ["" + name].concat(slice.call(args), ["(Done in " + ms + "ms)"]));
+      return this;
+    };
+
+    return Class;
+
+  })();
+
+  window.Class = Class;
+
+}).call(this);
+
+
+/* ---- plugins/UiConfig/media/js/lib/Promise.coffee ---- */
+
+
+(function() {
+  var Promise,
+    slice = [].slice;
+
+  Promise = (function() {
+    Promise.when = function() {
+      var args, fn, i, len, num_uncompleted, promise, task, task_id, tasks;
+      tasks = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+      num_uncompleted = tasks.length;
+      args = new Array(num_uncompleted);
+      promise = new Promise();
+      fn = function(task_id) {
+        return task.then(function() {
+          args[task_id] = Array.prototype.slice.call(arguments);
+          num_uncompleted--;
+          if (num_uncompleted === 0) {
+            return promise.complete.apply(promise, args);
+          }
+        });
+      };
+      for (task_id = i = 0, len = tasks.length; i < len; task_id = ++i) {
+        task = tasks[task_id];
+        fn(task_id);
+      }
+      return promise;
+    };
+
+    function Promise() {
+      this.resolved = false;
+      this.end_promise = null;
+      this.result = null;
+      this.callbacks = [];
+    }
+
+    Promise.prototype.resolve = function() {
+      var back, callback, i, len, ref;
+      if (this.resolved) {
+        return false;
+      }
+      this.resolved = true;
+      this.data = arguments;
+      if (!arguments.length) {
+        this.data = [true];
+      }
+      this.result = this.data[0];
+      ref = this.callbacks;
+      for (i = 0, len = ref.length; i < len; i++) {
+        callback = ref[i];
+        back = callback.apply(callback, this.data);
+      }
+      if (this.end_promise) {
+        return this.end_promise.resolve(back);
+      }
+    };
+
+    Promise.prototype.fail = function() {
+      return this.resolve(false);
+    };
+
+    Promise.prototype.then = function(callback) {
+      if (this.resolved === true) {
+        callback.apply(callback, this.data);
+        return;
+      }
+      this.callbacks.push(callback);
+      return this.end_promise = new Promise();
+    };
+
+    return Promise;
+
+  })();
+
+  window.Promise = Promise;
+
+
+  /*
+  s = Date.now()
+  log = (text) ->
+  	console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ")
+  
+  log "Started"
+  
+  cmd = (query) ->
+  	p = new Promise()
+  	setTimeout ( ->
+  		p.resolve query+" Result"
+  	), 100
+  	return p
+  
+  back = cmd("SELECT * FROM message").then (res) ->
+  	log res
+  	return "Return from query"
+  .then (res) ->
+  	log "Back then", res
+  
+  log "Query started", back
+   */
+
+}).call(this);
+
+
+/* ---- plugins/UiConfig/media/js/lib/Prototypes.coffee ---- */
+
+
+(function() {
+  String.prototype.startsWith = function(s) {
+    return this.slice(0, s.length) === s;
+  };
+
+  String.prototype.endsWith = function(s) {
+    return s === '' || this.slice(-s.length) === s;
+  };
+
+  String.prototype.repeat = function(count) {
+    return new Array(count + 1).join(this);
+  };
+
+  window.isEmpty = function(obj) {
+    var key;
+    for (key in obj) {
+      return false;
+    }
+    return true;
+  };
+
+}).call(this);
+
+
+/* ---- plugins/UiConfig/media/js/lib/maquette.js ---- */
+
+
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        // AMD. Register as an anonymous module.
+        define(['exports'], factory);
+    } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
+        // CommonJS
+        factory(exports);
+    } else {
+        // Browser globals
+        factory(root.maquette = {});
+    }
+}(this, function (exports) {
+    'use strict';
+    ;
+    ;
+    ;
+    ;
+    var NAMESPACE_W3 = 'http://www.w3.org/';
+    var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg';
+    var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink';
+    // Utilities
+    var emptyArray = [];
+    var extend = function (base, overrides) {
+        var result = {};
+        Object.keys(base).forEach(function (key) {
+            result[key] = base[key];
+        });
+        if (overrides) {
+            Object.keys(overrides).forEach(function (key) {
+                result[key] = overrides[key];
+            });
+        }
+        return result;
+    };
+    // Hyperscript helper functions
+    var same = function (vnode1, vnode2) {
+        if (vnode1.vnodeSelector !== vnode2.vnodeSelector) {
+            return false;
+        }
+        if (vnode1.properties && vnode2.properties) {
+            if (vnode1.properties.key !== vnode2.properties.key) {
+                return false;
+            }
+            return vnode1.properties.bind === vnode2.properties.bind;
+        }
+        return !vnode1.properties && !vnode2.properties;
+    };
+    var toTextVNode = function (data) {
+        return {
+            vnodeSelector: '',
+            properties: undefined,
+            children: undefined,
+            text: data.toString(),
+            domNode: null
+        };
+    };
+    var appendChildren = function (parentSelector, insertions, main) {
+        for (var i = 0; i < insertions.length; i++) {
+            var item = insertions[i];
+            if (Array.isArray(item)) {
+                appendChildren(parentSelector, item, main);
+            } else {
+                if (item !== null && item !== undefined) {
+                    if (!item.hasOwnProperty('vnodeSelector')) {
+                        item = toTextVNode(item);
+                    }
+                    main.push(item);
+                }
+            }
+        }
+    };
+    // Render helper functions
+    var missingTransition = function () {
+        throw new Error('Provide a transitions object to the projectionOptions to do animations');
+    };
+    var DEFAULT_PROJECTION_OPTIONS = {
+        namespace: undefined,
+        eventHandlerInterceptor: undefined,
+        styleApplyer: function (domNode, styleName, value) {
+            // Provides a hook to add vendor prefixes for browsers that still need it.
+            domNode.style[styleName] = value;
+        },
+        transitions: {
+            enter: missingTransition,
+            exit: missingTransition
+        }
+    };
+    var applyDefaultProjectionOptions = function (projectorOptions) {
+        return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
+    };
+    var checkStyleValue = function (styleValue) {
+        if (typeof styleValue !== 'string') {
+            throw new Error('Style values must be strings');
+        }
+    };
+    var setProperties = function (domNode, properties, projectionOptions) {
+        if (!properties) {
+            return;
+        }
+        var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor;
+        var propNames = Object.keys(properties);
+        var propCount = propNames.length;
+        for (var i = 0; i < propCount; i++) {
+            var propName = propNames[i];
+            /* tslint:disable:no-var-keyword: edge case */
+            var propValue = properties[propName];
+            /* tslint:enable:no-var-keyword */
+            if (propName === 'className') {
+                throw new Error('Property "className" is not supported, use "class".');
+            } else if (propName === 'class') {
+                if (domNode.className) {
+                    // May happen if classes is specified before class
+                    domNode.className += ' ' + propValue;
+                } else {
+                    domNode.className = propValue;
+                }
+            } else if (propName === 'classes') {
+                // object with string keys and boolean values
+                var classNames = Object.keys(propValue);
+                var classNameCount = classNames.length;
+                for (var j = 0; j < classNameCount; j++) {
+                    var className = classNames[j];
+                    if (propValue[className]) {
+                        domNode.classList.add(className);
+                    }
+                }
+            } else if (propName === 'styles') {
+                // object with string keys and string (!) values
+                var styleNames = Object.keys(propValue);
+                var styleCount = styleNames.length;
+                for (var j = 0; j < styleCount; j++) {
+                    var styleName = styleNames[j];
+                    var styleValue = propValue[styleName];
+                    if (styleValue) {
+                        checkStyleValue(styleValue);
+                        projectionOptions.styleApplyer(domNode, styleName, styleValue);
+                    }
+                }
+            } else if (propName === 'key') {
+                continue;
+            } else if (propValue === null || propValue === undefined) {
+                continue;
+            } else {
+                var type = typeof propValue;
+                if (type === 'function') {
+                    if (propName.lastIndexOf('on', 0) === 0) {
+                        if (eventHandlerInterceptor) {
+                            propValue = eventHandlerInterceptor(propName, propValue, domNode, properties);    // intercept eventhandlers
+                        }
+                        if (propName === 'oninput') {
+                            (function () {
+                                // record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput
+                                var oldPropValue = propValue;
+                                propValue = function (evt) {
+                                    evt.target['oninput-value'] = evt.target.value;
+                                    // may be HTMLTextAreaElement as well
+                                    oldPropValue.apply(this, [evt]);
+                                };
+                            }());
+                        }
+                        domNode[propName] = propValue;
+                    }
+                } else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') {
+                    if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
+                        domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
+                    } else {
+                        domNode.setAttribute(propName, propValue);
+                    }
+                } else {
+                    domNode[propName] = propValue;
+                }
+            }
+        }
+    };
+    var updateProperties = function (domNode, previousProperties, properties, projectionOptions) {
+        if (!properties) {
+            return;
+        }
+        var propertiesUpdated = false;
+        var propNames = Object.keys(properties);
+        var propCount = propNames.length;
+        for (var i = 0; i < propCount; i++) {
+            var propName = propNames[i];
+            // assuming that properties will be nullified instead of missing is by design
+            var propValue = properties[propName];
+            var previousValue = previousProperties[propName];
+            if (propName === 'class') {
+                if (previousValue !== propValue) {
+                    throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.');
+                }
+            } else if (propName === 'classes') {
+                var classList = domNode.classList;
+                var classNames = Object.keys(propValue);
+                var classNameCount = classNames.length;
+                for (var j = 0; j < classNameCount; j++) {
+                    var className = classNames[j];
+                    var on = !!propValue[className];
+                    var previousOn = !!previousValue[className];
+                    if (on === previousOn) {
+                        continue;
+                    }
+                    propertiesUpdated = true;
+                    if (on) {
+                        classList.add(className);
+                    } else {
+                        classList.remove(className);
+                    }
+                }
+            } else if (propName === 'styles') {
+                var styleNames = Object.keys(propValue);
+                var styleCount = styleNames.length;
+                for (var j = 0; j < styleCount; j++) {
+                    var styleName = styleNames[j];
+                    var newStyleValue = propValue[styleName];
+                    var oldStyleValue = previousValue[styleName];
+                    if (newStyleValue === oldStyleValue) {
+                        continue;
+                    }
+                    propertiesUpdated = true;
+                    if (newStyleValue) {
+                        checkStyleValue(newStyleValue);
+                        projectionOptions.styleApplyer(domNode, styleName, newStyleValue);
+                    } else {
+                        projectionOptions.styleApplyer(domNode, styleName, '');
+                    }
+                }
+            } else {
+                if (!propValue && typeof previousValue === 'string') {
+                    propValue = '';
+                }
+                if (propName === 'value') {
+                    if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) {
+                        domNode[propName] = propValue;
+                        // Reset the value, even if the virtual DOM did not change
+                        domNode['oninput-value'] = undefined;
+                    }
+                    // else do not update the domNode, otherwise the cursor position would be changed
+                    if (propValue !== previousValue) {
+                        propertiesUpdated = true;
+                    }
+                } else if (propValue !== previousValue) {
+                    var type = typeof propValue;
+                    if (type === 'function') {
+                        throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.');
+                    }
+                    if (type === 'string' && propName !== 'innerHTML') {
+                        if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
+                            domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
+                        } else {
+                            domNode.setAttribute(propName, propValue);
+                        }
+                    } else {
+                        if (domNode[propName] !== propValue) {
+                            domNode[propName] = propValue;
+                        }
+                    }
+                    propertiesUpdated = true;
+                }
+            }
+        }
+        return propertiesUpdated;
+    };
+    var findIndexOfChild = function (children, sameAs, start) {
+        if (sameAs.vnodeSelector !== '') {
+            // Never scan for text-nodes
+            for (var i = start; i < children.length; i++) {
+                if (same(children[i], sameAs)) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    };
+    var nodeAdded = function (vNode, transitions) {
+        if (vNode.properties) {
+            var enterAnimation = vNode.properties.enterAnimation;
+            if (enterAnimation) {
+                if (typeof enterAnimation === 'function') {
+                    enterAnimation(vNode.domNode, vNode.properties);
+                } else {
+                    transitions.enter(vNode.domNode, vNode.properties, enterAnimation);
+                }
+            }
+        }
+    };
+    var nodeToRemove = function (vNode, transitions) {
+        var domNode = vNode.domNode;
+        if (vNode.properties) {
+            var exitAnimation = vNode.properties.exitAnimation;
+            if (exitAnimation) {
+                domNode.style.pointerEvents = 'none';
+                var removeDomNode = function () {
+                    if (domNode.parentNode) {
+                        domNode.parentNode.removeChild(domNode);
+                    }
+                };
+                if (typeof exitAnimation === 'function') {
+                    exitAnimation(domNode, removeDomNode, vNode.properties);
+                    return;
+                } else {
+                    transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode);
+                    return;
+                }
+            }
+        }
+        if (domNode.parentNode) {
+            domNode.parentNode.removeChild(domNode);
+        }
+    };
+    var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) {
+        var childNode = childNodes[indexToCheck];
+        if (childNode.vnodeSelector === '') {
+            return;    // Text nodes need not be distinguishable
+        }
+        var properties = childNode.properties;
+        var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined;
+        if (!key) {
+            for (var i = 0; i < childNodes.length; i++) {
+                if (i !== indexToCheck) {
+                    var node = childNodes[i];
+                    if (same(node, childNode)) {
+                        if (operation === 'added') {
+                            throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.');
+                        } else {
+                            throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.');
+                        }
+                    }
+                }
+            }
+        }
+    };
+    var createDom;
+    var updateDom;
+    var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) {
+        if (oldChildren === newChildren) {
+            return false;
+        }
+        oldChildren = oldChildren || emptyArray;
+        newChildren = newChildren || emptyArray;
+        var oldChildrenLength = oldChildren.length;
+        var newChildrenLength = newChildren.length;
+        var transitions = projectionOptions.transitions;
+        var oldIndex = 0;
+        var newIndex = 0;
+        var i;
+        var textUpdated = false;
+        while (newIndex < newChildrenLength) {
+            var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined;
+            var newChild = newChildren[newIndex];
+            if (oldChild !== undefined && same(oldChild, newChild)) {
+                textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated;
+                oldIndex++;
+            } else {
+                var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1);
+                if (findOldIndex >= 0) {
+                    // Remove preceding missing children
+                    for (i = oldIndex; i < findOldIndex; i++) {
+                        nodeToRemove(oldChildren[i], transitions);
+                        checkDistinguishable(oldChildren, i, vnode, 'removed');
+                    }
+                    textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated;
+                    oldIndex = findOldIndex + 1;
+                } else {
+                    // New child
+                    createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions);
+                    nodeAdded(newChild, transitions);
+                    checkDistinguishable(newChildren, newIndex, vnode, 'added');
+                }
+            }
+            newIndex++;
+        }
+        if (oldChildrenLength > oldIndex) {
+            // Remove child fragments
+            for (i = oldIndex; i < oldChildrenLength; i++) {
+                nodeToRemove(oldChildren[i], transitions);
+                checkDistinguishable(oldChildren, i, vnode, 'removed');
+            }
+        }
+        return textUpdated;
+    };
+    var addChildren = function (domNode, children, projectionOptions) {
+        if (!children) {
+            return;
+        }
+        for (var i = 0; i < children.length; i++) {
+            createDom(children[i], domNode, undefined, projectionOptions);
+        }
+    };
+    var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) {
+        addChildren(domNode, vnode.children, projectionOptions);
+        // children before properties, needed for value property of <select>.
+        if (vnode.text) {
+            domNode.textContent = vnode.text;
+        }
+        setProperties(domNode, vnode.properties, projectionOptions);
+        if (vnode.properties && vnode.properties.afterCreate) {
+            vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
+        }
+    };
+    createDom = function (vnode, parentNode, insertBefore, projectionOptions) {
+        var domNode, i, c, start = 0, type, found;
+        var vnodeSelector = vnode.vnodeSelector;
+        if (vnodeSelector === '') {
+            domNode = vnode.domNode = document.createTextNode(vnode.text);
+            if (insertBefore !== undefined) {
+                parentNode.insertBefore(domNode, insertBefore);
+            } else {
+                parentNode.appendChild(domNode);
+            }
+        } else {
+            for (i = 0; i <= vnodeSelector.length; ++i) {
+                c = vnodeSelector.charAt(i);
+                if (i === vnodeSelector.length || c === '.' || c === '#') {
+                    type = vnodeSelector.charAt(start - 1);
+                    found = vnodeSelector.slice(start, i);
+                    if (type === '.') {
+                        domNode.classList.add(found);
+                    } else if (type === '#') {
+                        domNode.id = found;
+                    } else {
+                        if (found === 'svg') {
+                            projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
+                        }
+                        if (projectionOptions.namespace !== undefined) {
+                            domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found);
+                        } else {
+                            domNode = vnode.domNode = document.createElement(found);
+                        }
+                        if (insertBefore !== undefined) {
+                            parentNode.insertBefore(domNode, insertBefore);
+                        } else {
+                            parentNode.appendChild(domNode);
+                        }
+                    }
+                    start = i + 1;
+                }
+            }
+            initPropertiesAndChildren(domNode, vnode, projectionOptions);
+        }
+    };
+    updateDom = function (previous, vnode, projectionOptions) {
+        var domNode = previous.domNode;
+        var textUpdated = false;
+        if (previous === vnode) {
+            return false;    // By contract, VNode objects may not be modified anymore after passing them to maquette
+        }
+        var updated = false;
+        if (vnode.vnodeSelector === '') {
+            if (vnode.text !== previous.text) {
+                var newVNode = document.createTextNode(vnode.text);
+                domNode.parentNode.replaceChild(newVNode, domNode);
+                vnode.domNode = newVNode;
+                textUpdated = true;
+                return textUpdated;
+            }
+        } else {
+            if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) {
+                projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
+            }
+            if (previous.text !== vnode.text) {
+                updated = true;
+                if (vnode.text === undefined) {
+                    domNode.removeChild(domNode.firstChild);    // the only textnode presumably
+                } else {
+                    domNode.textContent = vnode.text;
+                }
+            }
+            updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
+            updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
+            if (vnode.properties && vnode.properties.afterUpdate) {
+                vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
+            }
+        }
+        if (updated && vnode.properties && vnode.properties.updateAnimation) {
+            vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties);
+        }
+        vnode.domNode = previous.domNode;
+        return textUpdated;
+    };
+    var createProjection = function (vnode, projectionOptions) {
+        return {
+            update: function (updatedVnode) {
+                if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
+                    throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)');
+                }
+                updateDom(vnode, updatedVnode, projectionOptions);
+                vnode = updatedVnode;
+            },
+            domNode: vnode.domNode
+        };
+    };
+    ;
+    // The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'.
+    exports.h = function (selector) {
+        var properties = arguments[1];
+        if (typeof selector !== 'string') {
+            throw new Error();
+        }
+        var childIndex = 1;
+        if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') {
+            childIndex = 2;
+        } else {
+            // Optional properties argument was omitted
+            properties = undefined;
+        }
+        var text = undefined;
+        var children = undefined;
+        var argsLength = arguments.length;
+        // Recognize a common special case where there is only a single text node
+        if (argsLength === childIndex + 1) {
+            var onlyChild = arguments[childIndex];
+            if (typeof onlyChild === 'string') {
+                text = onlyChild;
+            } else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') {
+                text = onlyChild[0];
+            }
+        }
+        if (text === undefined) {
+            children = [];
+            for (; childIndex < arguments.length; childIndex++) {
+                var child = arguments[childIndex];
+                if (child === null || child === undefined) {
+                    continue;
+                } else if (Array.isArray(child)) {
+                    appendChildren(selector, child, children);
+                } else if (child.hasOwnProperty('vnodeSelector')) {
+                    children.push(child);
+                } else {
+                    children.push(toTextVNode(child));
+                }
+            }
+        }
+        return {
+            vnodeSelector: selector,
+            properties: properties,
+            children: children,
+            text: text === '' ? undefined : text,
+            domNode: null
+        };
+    };
+    /**
+ * Contains simple low-level utility functions to manipulate the real DOM.
+ */
+    exports.dom = {
+        /**
+     * Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
+     * its [[Projection.domNode|domNode]] property.
+     * This is a low-level method. Users wil typically use a [[Projector]] instead.
+     * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
+     * objects may only be rendered once.
+     * @param projectionOptions - Options to be used to create and update the projection.
+     * @returns The [[Projection]] which also contains the DOM Node that was created.
+     */
+        create: function (vnode, projectionOptions) {
+            projectionOptions = applyDefaultProjectionOptions(projectionOptions);
+            createDom(vnode, document.createElement('div'), undefined, projectionOptions);
+            return createProjection(vnode, projectionOptions);
+        },
+        /**
+     * Appends a new childnode to the DOM which is generated from a [[VNode]].
+     * This is a low-level method. Users wil typically use a [[Projector]] instead.
+     * @param parentNode - The parent node for the new childNode.
+     * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
+     * objects may only be rendered once.
+     * @param projectionOptions - Options to be used to create and update the [[Projection]].
+     * @returns The [[Projection]] that was created.
+     */
+        append: function (parentNode, vnode, projectionOptions) {
+            projectionOptions = applyDefaultProjectionOptions(projectionOptions);
+            createDom(vnode, parentNode, undefined, projectionOptions);
+            return createProjection(vnode, projectionOptions);
+        },
+        /**
+     * Inserts a new DOM node which is generated from a [[VNode]].
+     * This is a low-level method. Users wil typically use a [[Projector]] instead.
+     * @param beforeNode - The node that the DOM Node is inserted before.
+     * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
+     * NOTE: [[VNode]] objects may only be rendered once.
+     * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
+     * @returns The [[Projection]] that was created.
+     */
+        insertBefore: function (beforeNode, vnode, projectionOptions) {
+            projectionOptions = applyDefaultProjectionOptions(projectionOptions);
+            createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions);
+            return createProjection(vnode, projectionOptions);
+        },
+        /**
+     * Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
+     * This means that the virtual DOM and the real DOM will have one overlapping element.
+     * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
+     * This is a low-level method. Users wil typically use a [[Projector]] instead.
+     * @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved.
+     * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
+     * may only be rendered once.
+     * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
+     * @returns The [[Projection]] that was created.
+     */
+        merge: function (element, vnode, projectionOptions) {
+            projectionOptions = applyDefaultProjectionOptions(projectionOptions);
+            vnode.domNode = element;
+            initPropertiesAndChildren(element, vnode, projectionOptions);
+            return createProjection(vnode, projectionOptions);
+        }
+    };
+    /**
+ * Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
+ * In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
+ * For more information, see [[CalculationCache]].
+ *
+ * @param <Result> The type of the value that is cached.
+ */
+    exports.createCache = function () {
+        var cachedInputs = undefined;
+        var cachedOutcome = undefined;
+        var result = {
+            invalidate: function () {
+                cachedOutcome = undefined;
+                cachedInputs = undefined;
+            },
+            result: function (inputs, calculation) {
+                if (cachedInputs) {
+                    for (var i = 0; i < inputs.length; i++) {
+                        if (cachedInputs[i] !== inputs[i]) {
+                            cachedOutcome = undefined;
+                        }
+                    }
+                }
+                if (!cachedOutcome) {
+                    cachedOutcome = calculation();
+                    cachedInputs = inputs;
+                }
+                return cachedOutcome;
+            }
+        };
+        return result;
+    };
+    /**
+ * Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
+ * See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}.
+ *
+ * @param <Source>       The type of source items. A database-record for instance.
+ * @param <Target>       The type of target items. A [[Component]] for instance.
+ * @param getSourceKey   `function(source)` that must return a key to identify each source object. The result must either be a string or a number.
+ * @param createResult   `function(source, index)` that must create a new result object from a given source. This function is identical
+ *                       to the `callback` argument in `Array.map(callback)`.
+ * @param updateResult   `function(source, target, index)` that updates a result to an updated source.
+ */
+    exports.createMapping = function (getSourceKey, createResult, updateResult) {
+        var keys = [];
+        var results = [];
+        return {
+            results: results,
+            map: function (newSources) {
+                var newKeys = newSources.map(getSourceKey);
+                var oldTargets = results.slice();
+                var oldIndex = 0;
+                for (var i = 0; i < newSources.length; i++) {
+                    var source = newSources[i];
+                    var sourceKey = newKeys[i];
+                    if (sourceKey === keys[oldIndex]) {
+                        results[i] = oldTargets[oldIndex];
+                        updateResult(source, oldTargets[oldIndex], i);
+                        oldIndex++;
+                    } else {
+                        var found = false;
+                        for (var j = 1; j < keys.length; j++) {
+                            var searchIndex = (oldIndex + j) % keys.length;
+                            if (keys[searchIndex] === sourceKey) {
+                                results[i] = oldTargets[searchIndex];
+                                updateResult(newSources[i], oldTargets[searchIndex], i);
+                                oldIndex = searchIndex + 1;
+                                found = true;
+                                break;
+                            }
+                        }
+                        if (!found) {
+                            results[i] = createResult(source, i);
+                        }
+                    }
+                }
+                results.length = newSources.length;
+                keys = newKeys;
+            }
+        };
+    };
+    /**
+ * Creates a [[Projector]] instance using the provided projectionOptions.
+ *
+ * For more information, see [[Projector]].
+ *
+ * @param projectionOptions   Options that influence how the DOM is rendered and updated.
+ */
+    exports.createProjector = function (projectorOptions) {
+        var projector;
+        var projectionOptions = applyDefaultProjectionOptions(projectorOptions);
+        projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) {
+            return function () {
+                // intercept function calls (event handlers) to do a render afterwards.
+                projector.scheduleRender();
+                return eventHandler.apply(properties.bind || this, arguments);
+            };
+        };
+        var renderCompleted = true;
+        var scheduled;
+        var stopped = false;
+        var projections = [];
+        var renderFunctions = [];
+        // matches the projections array
+        var doRender = function () {
+            scheduled = undefined;
+            if (!renderCompleted) {
+                return;    // The last render threw an error, it should be logged in the browser console.
+            }
+            renderCompleted = false;
+            for (var i = 0; i < projections.length; i++) {
+                var updatedVnode = renderFunctions[i]();
+                projections[i].update(updatedVnode);
+            }
+            renderCompleted = true;
+        };
+        projector = {
+            scheduleRender: function () {
+                if (!scheduled && !stopped) {
+                    scheduled = requestAnimationFrame(doRender);
+                }
+            },
+            stop: function () {
+                if (scheduled) {
+                    cancelAnimationFrame(scheduled);
+                    scheduled = undefined;
+                }
+                stopped = true;
+            },
+            resume: function () {
+                stopped = false;
+                renderCompleted = true;
+                projector.scheduleRender();
+            },
+            append: function (parentNode, renderMaquetteFunction) {
+                projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions));
+                renderFunctions.push(renderMaquetteFunction);
+            },
+            insertBefore: function (beforeNode, renderMaquetteFunction) {
+                projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions));
+                renderFunctions.push(renderMaquetteFunction);
+            },
+            merge: function (domNode, renderMaquetteFunction) {
+                projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions));
+                renderFunctions.push(renderMaquetteFunction);
+            },
+            replace: function (domNode, renderMaquetteFunction) {
+                var vnode = renderMaquetteFunction();
+                createDom(vnode, domNode.parentNode, domNode, projectionOptions);
+                domNode.parentNode.removeChild(domNode);
+                projections.push(createProjection(vnode, projectionOptions));
+                renderFunctions.push(renderMaquetteFunction);
+            },
+            detach: function (renderMaquetteFunction) {
+                for (var i = 0; i < renderFunctions.length; i++) {
+                    if (renderFunctions[i] === renderMaquetteFunction) {
+                        renderFunctions.splice(i, 1);
+                        return projections.splice(i, 1)[0];
+                    }
+                }
+                throw new Error('renderMaquetteFunction was not found');
+            }
+        };
+        return projector;
+    };
+}));
+
+
+
+/* ---- plugins/UiConfig/media/js/utils/Dollar.coffee ---- */
+
+
+(function() {
+  window.$ = function(selector) {
+    if (selector.startsWith("#")) {
+      return document.getElementById(selector.replace("#", ""));
+    }
+  };
+
+}).call(this);
+
+
+/* ---- plugins/UiConfig/media/js/utils/ZeroFrame.coffee ---- */
+
+
+(function() {
+  var ZeroFrame,
+    bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+    extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+    hasProp = {}.hasOwnProperty;
+
+  ZeroFrame = (function(superClass) {
+    extend(ZeroFrame, superClass);
+
+    function ZeroFrame(url) {
+      this.onCloseWebsocket = bind(this.onCloseWebsocket, this);
+      this.onOpenWebsocket = bind(this.onOpenWebsocket, this);
+      this.onRequest = bind(this.onRequest, this);
+      this.onMessage = bind(this.onMessage, this);
+      this.url = url;
+      this.waiting_cb = {};
+      this.wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1");
+      this.connect();
+      this.next_message_id = 1;
+      this.history_state = {};
+      this.init();
+    }
+
+    ZeroFrame.prototype.init = function() {
+      return this;
+    };
+
+    ZeroFrame.prototype.connect = function() {
+      this.target = window.parent;
+      window.addEventListener("message", this.onMessage, false);
+      this.cmd("innerReady");
+      window.addEventListener("beforeunload", (function(_this) {
+        return function(e) {
+          _this.log("save scrollTop", window.pageYOffset);
+          _this.history_state["scrollTop"] = window.pageYOffset;
+          return _this.cmd("wrapperReplaceState", [_this.history_state, null]);
+        };
+      })(this));
+      return this.cmd("wrapperGetState", [], (function(_this) {
+        return function(state) {
+          if (state != null) {
+            _this.history_state = state;
+          }
+          _this.log("restore scrollTop", state, window.pageYOffset);
+          if (window.pageYOffset === 0 && state) {
+            return window.scroll(window.pageXOffset, state.scrollTop);
+          }
+        };
+      })(this));
+    };
+
+    ZeroFrame.prototype.onMessage = function(e) {
+      var cmd, message;
+      message = e.data;
+      cmd = message.cmd;
+      if (cmd === "response") {
+        if (this.waiting_cb[message.to] != null) {
+          return this.waiting_cb[message.to](message.result);
+        } else {
+          return this.log("Websocket callback not found:", message);
+        }
+      } else if (cmd === "wrapperReady") {
+        return this.cmd("innerReady");
+      } else if (cmd === "ping") {
+        return this.response(message.id, "pong");
+      } else if (cmd === "wrapperOpenedWebsocket") {
+        return this.onOpenWebsocket();
+      } else if (cmd === "wrapperClosedWebsocket") {
+        return this.onCloseWebsocket();
+      } else {
+        return this.onRequest(cmd, message.params);
+      }
+    };
+
+    ZeroFrame.prototype.onRequest = function(cmd, message) {
+      return this.log("Unknown request", message);
+    };
+
+    ZeroFrame.prototype.response = function(to, result) {
+      return this.send({
+        "cmd": "response",
+        "to": to,
+        "result": result
+      });
+    };
+
+    ZeroFrame.prototype.cmd = function(cmd, params, cb) {
+      if (params == null) {
+        params = {};
+      }
+      if (cb == null) {
+        cb = null;
+      }
+      return this.send({
+        "cmd": cmd,
+        "params": params
+      }, cb);
+    };
+
+    ZeroFrame.prototype.send = function(message, cb) {
+      if (cb == null) {
+        cb = null;
+      }
+      message.wrapper_nonce = this.wrapper_nonce;
+      message.id = this.next_message_id;
+      this.next_message_id += 1;
+      this.target.postMessage(message, "*");
+      if (cb) {
+        return this.waiting_cb[message.id] = cb;
+      }
+    };
+
+    ZeroFrame.prototype.onOpenWebsocket = function() {
+      return this.log("Websocket open");
+    };
+
+    ZeroFrame.prototype.onCloseWebsocket = function() {
+      return this.log("Websocket close");
+    };
+
+    return ZeroFrame;
+
+  })(Class);
+
+  window.ZeroFrame = ZeroFrame;
+
+}).call(this);
+
+
+/* ---- plugins/UiConfig/media/js/ConfigStorage.coffee ---- */
+
+
+(function() {
+  var ConfigStorage,
+    bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+    extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+    hasProp = {}.hasOwnProperty;
+
+  ConfigStorage = (function(superClass) {
+    extend(ConfigStorage, superClass);
+
+    function ConfigStorage(config) {
+      this.config = config;
+      this.createSection = bind(this.createSection, this);
+      this.items = [];
+      this.createSections();
+      this.setValues(this.config);
+    }
+
+    ConfigStorage.prototype.setValues = function(values) {
+      var i, item, len, ref, results, section;
+      ref = this.items;
+      results = [];
+      for (i = 0, len = ref.length; i < len; i++) {
+        section = ref[i];
+        results.push((function() {
+          var j, len1, ref1, results1;
+          ref1 = section.items;
+          results1 = [];
+          for (j = 0, len1 = ref1.length; j < len1; j++) {
+            item = ref1[j];
+            if (!values[item.key]) {
+              continue;
+            }
+            item.value = this.formatValue(values[item.key].value);
+            item["default"] = this.formatValue(values[item.key]["default"]);
+            results1.push(item.pending = values[item.key].pending);
+          }
+          return results1;
+        }).call(this));
+      }
+      return results;
+    };
+
+    ConfigStorage.prototype.formatValue = function(value) {
+      if (!value) {
+        return false;
+      } else if (typeof value === "object") {
+        return value.join("\n");
+      } else if (typeof value === "number") {
+        return value.toString();
+      } else {
+        return value;
+      }
+    };
+
+    ConfigStorage.prototype.deformatValue = function(value, type) {
+      if (type === "object") {
+        return value.split("\n");
+      }
+      if (type === "boolean" && !value) {
+        return false;
+      } else {
+        return value;
+      }
+    };
+
+    ConfigStorage.prototype.createSections = function() {
+      var section;
+      section = this.createSection("Web Interface");
+      section.items.push({
+        key: "open_browser",
+        title: "Open web browser on ZeroNet startup",
+        type: "checkbox"
+      });
+      section = this.createSection("Network");
+      section.items.push({
+        key: "fileserver_port",
+        title: "File server port",
+        type: "text",
+        restrict: "number",
+        description: "Other peers will use this port to reach your served sites. (default: 15441)"
+      });
+      section.items.push({
+        title: "Tor",
+        key: "tor",
+        type: "select",
+        options: [
+          {
+            title: "Disable",
+            value: "disable"
+          }, {
+            title: "Enable",
+            value: "enable"
+          }, {
+            title: "Always",
+            value: "always"
+          }
+        ],
+        value: "Enable",
+        description: ["Disable: Don't connect to peers on Tor network", h("br"), "Enable: Only use Tor for Tor network peers", h("br"), "Always: Use Tor for every connections to hide your IP address (slower)"]
+      });
+      section.items.push({
+        title: "Use Tor bridges",
+        key: "tor_use_bridges",
+        type: "checkbox",
+        description: "Use obfuscated bridge relays to avoid network level Tor block (even slower)"
+      });
+      section.items.push({
+        title: "Trackers",
+        key: "trackers",
+        type: "textarea",
+        description: "Discover new peers using these adresses"
+      });
+      section.items.push({
+        title: "Trackers files",
+        key: "trackers_file",
+        type: "text",
+        description: "Load additional list of torrent trackers dynamically, from a file",
+        placeholder: "Eg.: data/trackers.json",
+        value_pos: "fullwidth"
+      });
+      return section.items.push({
+        title: "Proxy for tracker connections",
+        key: "trackers_proxy",
+        type: "select",
+        options: [
+          {
+            title: "Disable",
+            value: "disable"
+          }, {
+            title: "Tor",
+            value: "tor"
+          }
+        ]
+      });
+    };
+
+    ConfigStorage.prototype.createSection = function(title) {
+      var section;
+      section = {};
+      section.title = title;
+      section.items = [];
+      this.items.push(section);
+      return section;
+    };
+
+    return ConfigStorage;
+
+  })(Class);
+
+  window.ConfigStorage = ConfigStorage;
+
+}).call(this);
+
+
+/* ---- plugins/UiConfig/media/js/UiConfig.coffee ---- */
+
+
+(function() {
+  var UiConfig,
+    bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+    extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+    hasProp = {}.hasOwnProperty;
+
+  window.h = maquette.h;
+
+  UiConfig = (function(superClass) {
+    extend(UiConfig, superClass);
+
+    function UiConfig() {
+      this.renderBottomRestart = bind(this.renderBottomRestart, this);
+      this.handleRestartClick = bind(this.handleRestartClick, this);
+      this.renderBottomSave = bind(this.renderBottomSave, this);
+      this.handleSaveClick = bind(this.handleSaveClick, this);
+      this.render = bind(this.render, this);
+      this.renderValueSelect = bind(this.renderValueSelect, this);
+      this.renderValueCheckbox = bind(this.renderValueCheckbox, this);
+      this.renderValueTextarea = bind(this.renderValueTextarea, this);
+      this.autosizeTextarea = bind(this.autosizeTextarea, this);
+      this.renderValueText = bind(this.renderValueText, this);
+      this.handleCheckboxChange = bind(this.handleCheckboxChange, this);
+      this.handleInputChange = bind(this.handleInputChange, this);
+      this.renderSectionItem = bind(this.renderSectionItem, this);
+      this.handleResetClick = bind(this.handleResetClick, this);
+      this.renderSection = bind(this.renderSection, this);
+      this.saveValue = bind(this.saveValue, this);
+      this.saveValues = bind(this.saveValues, this);
+      this.getValuesPending = bind(this.getValuesPending, this);
+      this.getValuesChanged = bind(this.getValuesChanged, this);
+      this.createProjector = bind(this.createProjector, this);
+      this.updateConfig = bind(this.updateConfig, this);
+      this.onOpenWebsocket = bind(this.onOpenWebsocket, this);
+      return UiConfig.__super__.constructor.apply(this, arguments);
+    }
+
+    UiConfig.prototype.init = function() {
+      this.save_visible = true;
+      this.config = null;
+      this.values = null;
+      return window.onbeforeunload = (function(_this) {
+        return function() {
+          if (_this.getValuesChanged().length > 0) {
+            return true;
+          } else {
+            return null;
+          }
+        };
+      })(this);
+    };
+
+    UiConfig.prototype.onOpenWebsocket = function() {
+      this.cmd("wrapperSetTitle", "Config - ZeroNet");
+      return this.updateConfig();
+    };
+
+    UiConfig.prototype.updateConfig = function(cb) {
+      return this.cmd("configList", [], (function(_this) {
+        return function(res) {
+          var item, key;
+          _this.config = res;
+          _this.values = {};
+          _this.config_storage = new ConfigStorage(_this.config);
+          for (key in res) {
+            item = res[key];
+            _this.values[key] = _this.config_storage.formatValue(item.value);
+          }
+          _this.projector.scheduleRender();
+          return typeof cb === "function" ? cb() : void 0;
+        };
+      })(this));
+    };
+
+    UiConfig.prototype.createProjector = function() {
+      this.projector = maquette.createProjector();
+      this.projector.replace($("#content"), this.render);
+      this.projector.replace($("#bottom-save"), this.renderBottomSave);
+      return this.projector.replace($("#bottom-restart"), this.renderBottomRestart);
+    };
+
+    UiConfig.prototype.getValuesChanged = function() {
+      var key, ref, value, values_changed;
+      values_changed = [];
+      ref = this.values;
+      for (key in ref) {
+        value = ref[key];
+        if (this.config_storage.formatValue(value) !== this.config_storage.formatValue(this.config[key].value)) {
+          values_changed.push({
+            key: key,
+            value: value
+          });
+        }
+      }
+      return values_changed;
+    };
+
+    UiConfig.prototype.getValuesPending = function() {
+      var item, key, ref, values_pending;
+      values_pending = [];
+      ref = this.config;
+      for (key in ref) {
+        item = ref[key];
+        if (item.pending) {
+          values_pending.push(key);
+        }
+      }
+      return values_pending;
+    };
+
+    UiConfig.prototype.saveValues = function(cb) {
+      var changed_values, i, item, j, last, len, results, value, value_same_as_default;
+      changed_values = this.getValuesChanged();
+      results = [];
+      for (i = j = 0, len = changed_values.length; j < len; i = ++j) {
+        item = changed_values[i];
+        last = i === changed_values.length - 1;
+        value = this.config_storage.deformatValue(item.value, typeof this.config[item.key]["default"]);
+        value_same_as_default = JSON.stringify(this.config[item.key]["default"]) === JSON.stringify(value);
+        if (value_same_as_default) {
+          value = null;
+        }
+        results.push(this.saveValue(item.key, value, last ? cb : null));
+      }
+      return results;
+    };
+
+    UiConfig.prototype.saveValue = function(key, value, cb) {
+      if (key === "open_browser") {
+        if (value) {
+          value = "default_browser";
+        } else {
+          value = "False";
+        }
+      }
+      return Page.cmd("configSet", [key, value], (function(_this) {
+        return function(res) {
+          if (res !== "ok") {
+            Page.cmd("wrapperNotification", ["error", res.error]);
+          }
+          return typeof cb === "function" ? cb() : void 0;
+        };
+      })(this));
+    };
+
+    UiConfig.prototype.renderSection = function(section) {
+      return h("div.section", {
+        key: section.title
+      }, [h("h2", section.title), h("div.config-items", section.items.map(this.renderSectionItem))]);
+    };
+
+    UiConfig.prototype.handleResetClick = function(e) {
+      var config_key, default_value, node, ref;
+      node = e.currentTarget;
+      config_key = node.attributes.config_key.value;
+      default_value = (ref = node.attributes.default_value) != null ? ref.value : void 0;
+      return Page.cmd("wrapperConfirm", ["Reset " + config_key + " value?", "Reset to default"], (function(_this) {
+        return function(res) {
+          if (res) {
+            _this.values[config_key] = default_value;
+          }
+          return Page.projector.scheduleRender();
+        };
+      })(this));
+    };
+
+    UiConfig.prototype.renderSectionItem = function(item) {
+      var marker_title, ref, value_changed, value_default, value_pos;
+      value_pos = item.value_pos;
+      if (item.type === "textarea") {
+        if (value_pos == null) {
+          value_pos = "fullwidth";
+        }
+      } else {
+        if (value_pos == null) {
+          value_pos = "right";
+        }
+      }
+      value_changed = this.config_storage.formatValue(this.values[item.key]) !== item.value;
+      value_default = this.config_storage.formatValue(this.values[item.key]) === item["default"];
+      if ((ref = item.key) === "open_browser" || ref === "fileserver_port") {
+        value_default = true;
+      }
+      marker_title = "Changed from default value: " + item["default"] + " -> " + this.values[item.key];
+      if (item.pending) {
+        marker_title += " (change pending until client restart)";
+      }
+      return h("div.config-item", [
+        h("div.title", [h("h3", item.title), h("div.description", item.description)]), h("div.value.value-" + value_pos, item.type === "select" ? this.renderValueSelect(item) : item.type === "checkbox" ? this.renderValueCheckbox(item) : item.type === "textarea" ? this.renderValueTextarea(item) : this.renderValueText(item), h("a.marker", {
+          href: "#Reset",
+          title: marker_title,
+          onclick: this.handleResetClick,
+          config_key: item.key,
+          default_value: item["default"],
+          classes: {
+            "default": value_default,
+            changed: value_changed,
+            visible: !value_default || value_changed || item.pending,
+            pending: item.pending
+          }
+        }, "\u2022"))
+      ]);
+    };
+
+    UiConfig.prototype.handleInputChange = function(e) {
+      var config_key, node;
+      node = e.target;
+      config_key = node.attributes.config_key.value;
+      this.values[config_key] = node.value;
+      return Page.projector.scheduleRender();
+    };
+
+    UiConfig.prototype.handleCheckboxChange = function(e) {
+      var config_key, node, value;
+      node = e.currentTarget;
+      config_key = node.attributes.config_key.value;
+      value = !node.classList.contains("checked");
+      this.values[config_key] = value;
+      return Page.projector.scheduleRender();
+    };
+
+    UiConfig.prototype.renderValueText = function(item) {
+      var value;
+      value = this.values[item.key];
+      if (!value) {
+        value = "";
+      }
+      return h("input.input-" + item.type, {
+        type: item.type,
+        config_key: item.key,
+        value: value,
+        placeholder: item.placeholder,
+        oninput: this.handleInputChange
+      });
+    };
+
+    UiConfig.prototype.autosizeTextarea = function(e) {
+      var h, height_before, node, scrollh;
+      this.log("autosize", arguments);
+      if (e.currentTarget) {
+        node = e.currentTarget;
+      } else {
+        node = e;
+      }
+      height_before = node.style.height;
+      if (height_before) {
+        node.style.height = "0px";
+      }
+      h = node.offsetHeight;
+      scrollh = node.scrollHeight + 20;
+      if (scrollh > h) {
+        return node.style.height = scrollh + "px";
+      } else {
+        return node.style.height = height_before;
+      }
+    };
+
+    UiConfig.prototype.renderValueTextarea = function(item) {
+      var value;
+      value = this.values[item.key];
+      if (!value) {
+        value = "";
+      }
+      return h("textarea.input-" + item.type + ".input-text", {
+        type: item.type,
+        config_key: item.key,
+        oninput: this.handleInputChange,
+        afterCreate: this.autosizeTextarea,
+        updateAnimation: this.autosizeTextarea,
+        value: value
+      });
+    };
+
+    UiConfig.prototype.renderValueCheckbox = function(item) {
+      var checked;
+      if (this.values[item.key] && this.values[item.key] !== "False") {
+        checked = true;
+      } else {
+        checked = false;
+      }
+      return h("div.checkbox", {
+        onclick: this.handleCheckboxChange,
+        config_key: item.key,
+        classes: {
+          checked: checked
+        }
+      }, h("div.checkbox-skin"));
+    };
+
+    UiConfig.prototype.renderValueSelect = function(item) {
+      return h("select.input-select", {
+        config_key: item.key,
+        oninput: this.handleInputChange
+      }, item.options.map((function(_this) {
+        return function(option) {
+          return h("option", {
+            selected: option.value === _this.values[item.key],
+            value: option.value
+          }, option.title);
+        };
+      })(this)));
+    };
+
+    UiConfig.prototype.render = function() {
+      if (!this.config) {
+        return h("div.content");
+      }
+      return h("div.content", [this.config_storage.items.map(this.renderSection)]);
+    };
+
+    UiConfig.prototype.handleSaveClick = function() {
+      this.save_loading = true;
+      this.logStart("Save");
+      this.saveValues((function(_this) {
+        return function() {
+          _this.save_loading = false;
+          _this.logEnd("Save");
+          _this.updateConfig();
+          return Page.projector.scheduleRender();
+        };
+      })(this));
+      return false;
+    };
+
+    UiConfig.prototype.renderBottomSave = function() {
+      var values_changed;
+      values_changed = this.getValuesChanged();
+      return h("div.bottom.bottom-save", {
+        classes: {
+          visible: values_changed.length
+        }
+      }, h("div.bottom-content", [
+        h("div.title", values_changed.length + " configuration item value changed"), h("a.button.button-submit.button-save", {
+          href: "#Save",
+          classes: {
+            loading: this.save_loading
+          },
+          onclick: this.handleSaveClick
+        }, "Save settings")
+      ]));
+    };
+
+    UiConfig.prototype.handleRestartClick = function() {
+      this.restart_loading = true;
+      Page.cmd("serverShutdown", {
+        restart: true
+      });
+      Page.projector.scheduleRender();
+      return false;
+    };
+
+    UiConfig.prototype.renderBottomRestart = function() {
+      var values_changed, values_pending;
+      values_pending = this.getValuesPending();
+      values_changed = this.getValuesChanged();
+      return h("div.bottom.bottom-restart", {
+        classes: {
+          visible: values_pending.length && !values_changed.length
+        }
+      }, h("div.bottom-content", [
+        h("div.title", "Some changes settings requires restart"), h("a.button.button-submit.button-restart", {
+          href: "#Restart",
+          classes: {
+            loading: this.restart_loading
+          },
+          onclick: this.handleRestartClick
+        }, "Restart ZeroNet client")
+      ]));
+    };
+
+    return UiConfig;
+
+  })(ZeroFrame);
+
+  window.Page = new UiConfig();
+
+  window.Page.createProjector();
+
+}).call(this);

+ 23 - 0
plugins/UiConfig/media/js/lib/Class.coffee

@@ -0,0 +1,23 @@
+class Class
+	trace: true
+
+	log: (args...) ->
+		return unless @trace
+		return if typeof console is 'undefined'
+		args.unshift("[#{@.constructor.name}]")
+		console.log(args...)
+		@
+		
+	logStart: (name, args...) ->
+		return unless @trace
+		@logtimers or= {}
+		@logtimers[name] = +(new Date)
+		@log "#{name}", args..., "(started)" if args.length > 0
+		@
+		
+	logEnd: (name, args...) ->
+		ms = +(new Date)-@logtimers[name]
+		@log "#{name}", args..., "(Done in #{ms}ms)"
+		@ 
+
+window.Class = Class

+ 74 - 0
plugins/UiConfig/media/js/lib/Promise.coffee

@@ -0,0 +1,74 @@
+# From: http://dev.bizo.com/2011/12/promises-in-javascriptcoffeescript.html
+
+class Promise
+	@when: (tasks...) ->
+		num_uncompleted = tasks.length
+		args = new Array(num_uncompleted)
+		promise = new Promise()
+
+		for task, task_id in tasks
+			((task_id) ->
+				task.then(() ->
+					args[task_id] = Array.prototype.slice.call(arguments)
+					num_uncompleted--
+					promise.complete.apply(promise, args) if num_uncompleted == 0
+				)
+			)(task_id)
+
+		return promise
+
+	constructor: ->
+		@resolved = false
+		@end_promise = null
+		@result = null
+		@callbacks = []
+
+	resolve: ->
+		if @resolved
+			return false
+		@resolved = true
+		@data = arguments
+		if not arguments.length
+			@data = [true]
+		@result = @data[0]
+		for callback in @callbacks
+			back = callback.apply callback, @data
+		if @end_promise
+			@end_promise.resolve(back)
+
+	fail: ->
+		@resolve(false)
+
+	then: (callback) ->
+		if @resolved == true
+			callback.apply callback, @data
+			return
+
+		@callbacks.push callback
+
+		@end_promise = new Promise()
+
+window.Promise = Promise
+
+###
+s = Date.now()
+log = (text) ->
+	console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ")
+
+log "Started"
+
+cmd = (query) ->
+	p = new Promise()
+	setTimeout ( ->
+		p.resolve query+" Result"
+	), 100
+	return p
+
+back = cmd("SELECT * FROM message").then (res) ->
+	log res
+	return "Return from query"
+.then (res) ->
+	log "Back then", res
+
+log "Query started", back
+###

+ 8 - 0
plugins/UiConfig/media/js/lib/Prototypes.coffee

@@ -0,0 +1,8 @@
+String::startsWith = (s) -> @[...s.length] is s
+String::endsWith = (s) -> s is '' or @[-s.length..] is s
+String::repeat = (count) -> new Array( count + 1 ).join(@)
+
+window.isEmpty = (obj) ->
+	for key of obj
+		return false
+	return true

+ 770 - 0
plugins/UiConfig/media/js/lib/maquette.js

@@ -0,0 +1,770 @@
+(function (root, factory) {
+    if (typeof define === 'function' && define.amd) {
+        // AMD. Register as an anonymous module.
+        define(['exports'], factory);
+    } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
+        // CommonJS
+        factory(exports);
+    } else {
+        // Browser globals
+        factory(root.maquette = {});
+    }
+}(this, function (exports) {
+    'use strict';
+    ;
+    ;
+    ;
+    ;
+    var NAMESPACE_W3 = 'http://www.w3.org/';
+    var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg';
+    var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink';
+    // Utilities
+    var emptyArray = [];
+    var extend = function (base, overrides) {
+        var result = {};
+        Object.keys(base).forEach(function (key) {
+            result[key] = base[key];
+        });
+        if (overrides) {
+            Object.keys(overrides).forEach(function (key) {
+                result[key] = overrides[key];
+            });
+        }
+        return result;
+    };
+    // Hyperscript helper functions
+    var same = function (vnode1, vnode2) {
+        if (vnode1.vnodeSelector !== vnode2.vnodeSelector) {
+            return false;
+        }
+        if (vnode1.properties && vnode2.properties) {
+            if (vnode1.properties.key !== vnode2.properties.key) {
+                return false;
+            }
+            return vnode1.properties.bind === vnode2.properties.bind;
+        }
+        return !vnode1.properties && !vnode2.properties;
+    };
+    var toTextVNode = function (data) {
+        return {
+            vnodeSelector: '',
+            properties: undefined,
+            children: undefined,
+            text: data.toString(),
+            domNode: null
+        };
+    };
+    var appendChildren = function (parentSelector, insertions, main) {
+        for (var i = 0; i < insertions.length; i++) {
+            var item = insertions[i];
+            if (Array.isArray(item)) {
+                appendChildren(parentSelector, item, main);
+            } else {
+                if (item !== null && item !== undefined) {
+                    if (!item.hasOwnProperty('vnodeSelector')) {
+                        item = toTextVNode(item);
+                    }
+                    main.push(item);
+                }
+            }
+        }
+    };
+    // Render helper functions
+    var missingTransition = function () {
+        throw new Error('Provide a transitions object to the projectionOptions to do animations');
+    };
+    var DEFAULT_PROJECTION_OPTIONS = {
+        namespace: undefined,
+        eventHandlerInterceptor: undefined,
+        styleApplyer: function (domNode, styleName, value) {
+            // Provides a hook to add vendor prefixes for browsers that still need it.
+            domNode.style[styleName] = value;
+        },
+        transitions: {
+            enter: missingTransition,
+            exit: missingTransition
+        }
+    };
+    var applyDefaultProjectionOptions = function (projectorOptions) {
+        return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
+    };
+    var checkStyleValue = function (styleValue) {
+        if (typeof styleValue !== 'string') {
+            throw new Error('Style values must be strings');
+        }
+    };
+    var setProperties = function (domNode, properties, projectionOptions) {
+        if (!properties) {
+            return;
+        }
+        var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor;
+        var propNames = Object.keys(properties);
+        var propCount = propNames.length;
+        for (var i = 0; i < propCount; i++) {
+            var propName = propNames[i];
+            /* tslint:disable:no-var-keyword: edge case */
+            var propValue = properties[propName];
+            /* tslint:enable:no-var-keyword */
+            if (propName === 'className') {
+                throw new Error('Property "className" is not supported, use "class".');
+            } else if (propName === 'class') {
+                if (domNode.className) {
+                    // May happen if classes is specified before class
+                    domNode.className += ' ' + propValue;
+                } else {
+                    domNode.className = propValue;
+                }
+            } else if (propName === 'classes') {
+                // object with string keys and boolean values
+                var classNames = Object.keys(propValue);
+                var classNameCount = classNames.length;
+                for (var j = 0; j < classNameCount; j++) {
+                    var className = classNames[j];
+                    if (propValue[className]) {
+                        domNode.classList.add(className);
+                    }
+                }
+            } else if (propName === 'styles') {
+                // object with string keys and string (!) values
+                var styleNames = Object.keys(propValue);
+                var styleCount = styleNames.length;
+                for (var j = 0; j < styleCount; j++) {
+                    var styleName = styleNames[j];
+                    var styleValue = propValue[styleName];
+                    if (styleValue) {
+                        checkStyleValue(styleValue);
+                        projectionOptions.styleApplyer(domNode, styleName, styleValue);
+                    }
+                }
+            } else if (propName === 'key') {
+                continue;
+            } else if (propValue === null || propValue === undefined) {
+                continue;
+            } else {
+                var type = typeof propValue;
+                if (type === 'function') {
+                    if (propName.lastIndexOf('on', 0) === 0) {
+                        if (eventHandlerInterceptor) {
+                            propValue = eventHandlerInterceptor(propName, propValue, domNode, properties);    // intercept eventhandlers
+                        }
+                        if (propName === 'oninput') {
+                            (function () {
+                                // record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput
+                                var oldPropValue = propValue;
+                                propValue = function (evt) {
+                                    evt.target['oninput-value'] = evt.target.value;
+                                    // may be HTMLTextAreaElement as well
+                                    oldPropValue.apply(this, [evt]);
+                                };
+                            }());
+                        }
+                        domNode[propName] = propValue;
+                    }
+                } else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') {
+                    if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
+                        domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
+                    } else {
+                        domNode.setAttribute(propName, propValue);
+                    }
+                } else {
+                    domNode[propName] = propValue;
+                }
+            }
+        }
+    };
+    var updateProperties = function (domNode, previousProperties, properties, projectionOptions) {
+        if (!properties) {
+            return;
+        }
+        var propertiesUpdated = false;
+        var propNames = Object.keys(properties);
+        var propCount = propNames.length;
+        for (var i = 0; i < propCount; i++) {
+            var propName = propNames[i];
+            // assuming that properties will be nullified instead of missing is by design
+            var propValue = properties[propName];
+            var previousValue = previousProperties[propName];
+            if (propName === 'class') {
+                if (previousValue !== propValue) {
+                    throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.');
+                }
+            } else if (propName === 'classes') {
+                var classList = domNode.classList;
+                var classNames = Object.keys(propValue);
+                var classNameCount = classNames.length;
+                for (var j = 0; j < classNameCount; j++) {
+                    var className = classNames[j];
+                    var on = !!propValue[className];
+                    var previousOn = !!previousValue[className];
+                    if (on === previousOn) {
+                        continue;
+                    }
+                    propertiesUpdated = true;
+                    if (on) {
+                        classList.add(className);
+                    } else {
+                        classList.remove(className);
+                    }
+                }
+            } else if (propName === 'styles') {
+                var styleNames = Object.keys(propValue);
+                var styleCount = styleNames.length;
+                for (var j = 0; j < styleCount; j++) {
+                    var styleName = styleNames[j];
+                    var newStyleValue = propValue[styleName];
+                    var oldStyleValue = previousValue[styleName];
+                    if (newStyleValue === oldStyleValue) {
+                        continue;
+                    }
+                    propertiesUpdated = true;
+                    if (newStyleValue) {
+                        checkStyleValue(newStyleValue);
+                        projectionOptions.styleApplyer(domNode, styleName, newStyleValue);
+                    } else {
+                        projectionOptions.styleApplyer(domNode, styleName, '');
+                    }
+                }
+            } else {
+                if (!propValue && typeof previousValue === 'string') {
+                    propValue = '';
+                }
+                if (propName === 'value') {
+                    if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) {
+                        domNode[propName] = propValue;
+                        // Reset the value, even if the virtual DOM did not change
+                        domNode['oninput-value'] = undefined;
+                    }
+                    // else do not update the domNode, otherwise the cursor position would be changed
+                    if (propValue !== previousValue) {
+                        propertiesUpdated = true;
+                    }
+                } else if (propValue !== previousValue) {
+                    var type = typeof propValue;
+                    if (type === 'function') {
+                        throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.');
+                    }
+                    if (type === 'string' && propName !== 'innerHTML') {
+                        if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
+                            domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
+                        } else {
+                            domNode.setAttribute(propName, propValue);
+                        }
+                    } else {
+                        if (domNode[propName] !== propValue) {
+                            domNode[propName] = propValue;
+                        }
+                    }
+                    propertiesUpdated = true;
+                }
+            }
+        }
+        return propertiesUpdated;
+    };
+    var findIndexOfChild = function (children, sameAs, start) {
+        if (sameAs.vnodeSelector !== '') {
+            // Never scan for text-nodes
+            for (var i = start; i < children.length; i++) {
+                if (same(children[i], sameAs)) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    };
+    var nodeAdded = function (vNode, transitions) {
+        if (vNode.properties) {
+            var enterAnimation = vNode.properties.enterAnimation;
+            if (enterAnimation) {
+                if (typeof enterAnimation === 'function') {
+                    enterAnimation(vNode.domNode, vNode.properties);
+                } else {
+                    transitions.enter(vNode.domNode, vNode.properties, enterAnimation);
+                }
+            }
+        }
+    };
+    var nodeToRemove = function (vNode, transitions) {
+        var domNode = vNode.domNode;
+        if (vNode.properties) {
+            var exitAnimation = vNode.properties.exitAnimation;
+            if (exitAnimation) {
+                domNode.style.pointerEvents = 'none';
+                var removeDomNode = function () {
+                    if (domNode.parentNode) {
+                        domNode.parentNode.removeChild(domNode);
+                    }
+                };
+                if (typeof exitAnimation === 'function') {
+                    exitAnimation(domNode, removeDomNode, vNode.properties);
+                    return;
+                } else {
+                    transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode);
+                    return;
+                }
+            }
+        }
+        if (domNode.parentNode) {
+            domNode.parentNode.removeChild(domNode);
+        }
+    };
+    var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) {
+        var childNode = childNodes[indexToCheck];
+        if (childNode.vnodeSelector === '') {
+            return;    // Text nodes need not be distinguishable
+        }
+        var properties = childNode.properties;
+        var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined;
+        if (!key) {
+            for (var i = 0; i < childNodes.length; i++) {
+                if (i !== indexToCheck) {
+                    var node = childNodes[i];
+                    if (same(node, childNode)) {
+                        if (operation === 'added') {
+                            throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.');
+                        } else {
+                            throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.');
+                        }
+                    }
+                }
+            }
+        }
+    };
+    var createDom;
+    var updateDom;
+    var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) {
+        if (oldChildren === newChildren) {
+            return false;
+        }
+        oldChildren = oldChildren || emptyArray;
+        newChildren = newChildren || emptyArray;
+        var oldChildrenLength = oldChildren.length;
+        var newChildrenLength = newChildren.length;
+        var transitions = projectionOptions.transitions;
+        var oldIndex = 0;
+        var newIndex = 0;
+        var i;
+        var textUpdated = false;
+        while (newIndex < newChildrenLength) {
+            var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined;
+            var newChild = newChildren[newIndex];
+            if (oldChild !== undefined && same(oldChild, newChild)) {
+                textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated;
+                oldIndex++;
+            } else {
+                var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1);
+                if (findOldIndex >= 0) {
+                    // Remove preceding missing children
+                    for (i = oldIndex; i < findOldIndex; i++) {
+                        nodeToRemove(oldChildren[i], transitions);
+                        checkDistinguishable(oldChildren, i, vnode, 'removed');
+                    }
+                    textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated;
+                    oldIndex = findOldIndex + 1;
+                } else {
+                    // New child
+                    createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions);
+                    nodeAdded(newChild, transitions);
+                    checkDistinguishable(newChildren, newIndex, vnode, 'added');
+                }
+            }
+            newIndex++;
+        }
+        if (oldChildrenLength > oldIndex) {
+            // Remove child fragments
+            for (i = oldIndex; i < oldChildrenLength; i++) {
+                nodeToRemove(oldChildren[i], transitions);
+                checkDistinguishable(oldChildren, i, vnode, 'removed');
+            }
+        }
+        return textUpdated;
+    };
+    var addChildren = function (domNode, children, projectionOptions) {
+        if (!children) {
+            return;
+        }
+        for (var i = 0; i < children.length; i++) {
+            createDom(children[i], domNode, undefined, projectionOptions);
+        }
+    };
+    var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) {
+        addChildren(domNode, vnode.children, projectionOptions);
+        // children before properties, needed for value property of <select>.
+        if (vnode.text) {
+            domNode.textContent = vnode.text;
+        }
+        setProperties(domNode, vnode.properties, projectionOptions);
+        if (vnode.properties && vnode.properties.afterCreate) {
+            vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
+        }
+    };
+    createDom = function (vnode, parentNode, insertBefore, projectionOptions) {
+        var domNode, i, c, start = 0, type, found;
+        var vnodeSelector = vnode.vnodeSelector;
+        if (vnodeSelector === '') {
+            domNode = vnode.domNode = document.createTextNode(vnode.text);
+            if (insertBefore !== undefined) {
+                parentNode.insertBefore(domNode, insertBefore);
+            } else {
+                parentNode.appendChild(domNode);
+            }
+        } else {
+            for (i = 0; i <= vnodeSelector.length; ++i) {
+                c = vnodeSelector.charAt(i);
+                if (i === vnodeSelector.length || c === '.' || c === '#') {
+                    type = vnodeSelector.charAt(start - 1);
+                    found = vnodeSelector.slice(start, i);
+                    if (type === '.') {
+                        domNode.classList.add(found);
+                    } else if (type === '#') {
+                        domNode.id = found;
+                    } else {
+                        if (found === 'svg') {
+                            projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
+                        }
+                        if (projectionOptions.namespace !== undefined) {
+                            domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found);
+                        } else {
+                            domNode = vnode.domNode = document.createElement(found);
+                        }
+                        if (insertBefore !== undefined) {
+                            parentNode.insertBefore(domNode, insertBefore);
+                        } else {
+                            parentNode.appendChild(domNode);
+                        }
+                    }
+                    start = i + 1;
+                }
+            }
+            initPropertiesAndChildren(domNode, vnode, projectionOptions);
+        }
+    };
+    updateDom = function (previous, vnode, projectionOptions) {
+        var domNode = previous.domNode;
+        var textUpdated = false;
+        if (previous === vnode) {
+            return false;    // By contract, VNode objects may not be modified anymore after passing them to maquette
+        }
+        var updated = false;
+        if (vnode.vnodeSelector === '') {
+            if (vnode.text !== previous.text) {
+                var newVNode = document.createTextNode(vnode.text);
+                domNode.parentNode.replaceChild(newVNode, domNode);
+                vnode.domNode = newVNode;
+                textUpdated = true;
+                return textUpdated;
+            }
+        } else {
+            if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) {
+                projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
+            }
+            if (previous.text !== vnode.text) {
+                updated = true;
+                if (vnode.text === undefined) {
+                    domNode.removeChild(domNode.firstChild);    // the only textnode presumably
+                } else {
+                    domNode.textContent = vnode.text;
+                }
+            }
+            updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
+            updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
+            if (vnode.properties && vnode.properties.afterUpdate) {
+                vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
+            }
+        }
+        if (updated && vnode.properties && vnode.properties.updateAnimation) {
+            vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties);
+        }
+        vnode.domNode = previous.domNode;
+        return textUpdated;
+    };
+    var createProjection = function (vnode, projectionOptions) {
+        return {
+            update: function (updatedVnode) {
+                if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
+                    throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)');
+                }
+                updateDom(vnode, updatedVnode, projectionOptions);
+                vnode = updatedVnode;
+            },
+            domNode: vnode.domNode
+        };
+    };
+    ;
+    // The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'.
+    exports.h = function (selector) {
+        var properties = arguments[1];
+        if (typeof selector !== 'string') {
+            throw new Error();
+        }
+        var childIndex = 1;
+        if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') {
+            childIndex = 2;
+        } else {
+            // Optional properties argument was omitted
+            properties = undefined;
+        }
+        var text = undefined;
+        var children = undefined;
+        var argsLength = arguments.length;
+        // Recognize a common special case where there is only a single text node
+        if (argsLength === childIndex + 1) {
+            var onlyChild = arguments[childIndex];
+            if (typeof onlyChild === 'string') {
+                text = onlyChild;
+            } else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') {
+                text = onlyChild[0];
+            }
+        }
+        if (text === undefined) {
+            children = [];
+            for (; childIndex < arguments.length; childIndex++) {
+                var child = arguments[childIndex];
+                if (child === null || child === undefined) {
+                    continue;
+                } else if (Array.isArray(child)) {
+                    appendChildren(selector, child, children);
+                } else if (child.hasOwnProperty('vnodeSelector')) {
+                    children.push(child);
+                } else {
+                    children.push(toTextVNode(child));
+                }
+            }
+        }
+        return {
+            vnodeSelector: selector,
+            properties: properties,
+            children: children,
+            text: text === '' ? undefined : text,
+            domNode: null
+        };
+    };
+    /**
+ * Contains simple low-level utility functions to manipulate the real DOM.
+ */
+    exports.dom = {
+        /**
+     * Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
+     * its [[Projection.domNode|domNode]] property.
+     * This is a low-level method. Users wil typically use a [[Projector]] instead.
+     * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
+     * objects may only be rendered once.
+     * @param projectionOptions - Options to be used to create and update the projection.
+     * @returns The [[Projection]] which also contains the DOM Node that was created.
+     */
+        create: function (vnode, projectionOptions) {
+            projectionOptions = applyDefaultProjectionOptions(projectionOptions);
+            createDom(vnode, document.createElement('div'), undefined, projectionOptions);
+            return createProjection(vnode, projectionOptions);
+        },
+        /**
+     * Appends a new childnode to the DOM which is generated from a [[VNode]].
+     * This is a low-level method. Users wil typically use a [[Projector]] instead.
+     * @param parentNode - The parent node for the new childNode.
+     * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
+     * objects may only be rendered once.
+     * @param projectionOptions - Options to be used to create and update the [[Projection]].
+     * @returns The [[Projection]] that was created.
+     */
+        append: function (parentNode, vnode, projectionOptions) {
+            projectionOptions = applyDefaultProjectionOptions(projectionOptions);
+            createDom(vnode, parentNode, undefined, projectionOptions);
+            return createProjection(vnode, projectionOptions);
+        },
+        /**
+     * Inserts a new DOM node which is generated from a [[VNode]].
+     * This is a low-level method. Users wil typically use a [[Projector]] instead.
+     * @param beforeNode - The node that the DOM Node is inserted before.
+     * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
+     * NOTE: [[VNode]] objects may only be rendered once.
+     * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
+     * @returns The [[Projection]] that was created.
+     */
+        insertBefore: function (beforeNode, vnode, projectionOptions) {
+            projectionOptions = applyDefaultProjectionOptions(projectionOptions);
+            createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions);
+            return createProjection(vnode, projectionOptions);
+        },
+        /**
+     * Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
+     * This means that the virtual DOM and the real DOM will have one overlapping element.
+     * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
+     * This is a low-level method. Users wil typically use a [[Projector]] instead.
+     * @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved.
+     * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
+     * may only be rendered once.
+     * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
+     * @returns The [[Projection]] that was created.
+     */
+        merge: function (element, vnode, projectionOptions) {
+            projectionOptions = applyDefaultProjectionOptions(projectionOptions);
+            vnode.domNode = element;
+            initPropertiesAndChildren(element, vnode, projectionOptions);
+            return createProjection(vnode, projectionOptions);
+        }
+    };
+    /**
+ * Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
+ * In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
+ * For more information, see [[CalculationCache]].
+ *
+ * @param <Result> The type of the value that is cached.
+ */
+    exports.createCache = function () {
+        var cachedInputs = undefined;
+        var cachedOutcome = undefined;
+        var result = {
+            invalidate: function () {
+                cachedOutcome = undefined;
+                cachedInputs = undefined;
+            },
+            result: function (inputs, calculation) {
+                if (cachedInputs) {
+                    for (var i = 0; i < inputs.length; i++) {
+                        if (cachedInputs[i] !== inputs[i]) {
+                            cachedOutcome = undefined;
+                        }
+                    }
+                }
+                if (!cachedOutcome) {
+                    cachedOutcome = calculation();
+                    cachedInputs = inputs;
+                }
+                return cachedOutcome;
+            }
+        };
+        return result;
+    };
+    /**
+ * Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
+ * See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}.
+ *
+ * @param <Source>       The type of source items. A database-record for instance.
+ * @param <Target>       The type of target items. A [[Component]] for instance.
+ * @param getSourceKey   `function(source)` that must return a key to identify each source object. The result must either be a string or a number.
+ * @param createResult   `function(source, index)` that must create a new result object from a given source. This function is identical
+ *                       to the `callback` argument in `Array.map(callback)`.
+ * @param updateResult   `function(source, target, index)` that updates a result to an updated source.
+ */
+    exports.createMapping = function (getSourceKey, createResult, updateResult) {
+        var keys = [];
+        var results = [];
+        return {
+            results: results,
+            map: function (newSources) {
+                var newKeys = newSources.map(getSourceKey);
+                var oldTargets = results.slice();
+                var oldIndex = 0;
+                for (var i = 0; i < newSources.length; i++) {
+                    var source = newSources[i];
+                    var sourceKey = newKeys[i];
+                    if (sourceKey === keys[oldIndex]) {
+                        results[i] = oldTargets[oldIndex];
+                        updateResult(source, oldTargets[oldIndex], i);
+                        oldIndex++;
+                    } else {
+                        var found = false;
+                        for (var j = 1; j < keys.length; j++) {
+                            var searchIndex = (oldIndex + j) % keys.length;
+                            if (keys[searchIndex] === sourceKey) {
+                                results[i] = oldTargets[searchIndex];
+                                updateResult(newSources[i], oldTargets[searchIndex], i);
+                                oldIndex = searchIndex + 1;
+                                found = true;
+                                break;
+                            }
+                        }
+                        if (!found) {
+                            results[i] = createResult(source, i);
+                        }
+                    }
+                }
+                results.length = newSources.length;
+                keys = newKeys;
+            }
+        };
+    };
+    /**
+ * Creates a [[Projector]] instance using the provided projectionOptions.
+ *
+ * For more information, see [[Projector]].
+ *
+ * @param projectionOptions   Options that influence how the DOM is rendered and updated.
+ */
+    exports.createProjector = function (projectorOptions) {
+        var projector;
+        var projectionOptions = applyDefaultProjectionOptions(projectorOptions);
+        projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) {
+            return function () {
+                // intercept function calls (event handlers) to do a render afterwards.
+                projector.scheduleRender();
+                return eventHandler.apply(properties.bind || this, arguments);
+            };
+        };
+        var renderCompleted = true;
+        var scheduled;
+        var stopped = false;
+        var projections = [];
+        var renderFunctions = [];
+        // matches the projections array
+        var doRender = function () {
+            scheduled = undefined;
+            if (!renderCompleted) {
+                return;    // The last render threw an error, it should be logged in the browser console.
+            }
+            renderCompleted = false;
+            for (var i = 0; i < projections.length; i++) {
+                var updatedVnode = renderFunctions[i]();
+                projections[i].update(updatedVnode);
+            }
+            renderCompleted = true;
+        };
+        projector = {
+            scheduleRender: function () {
+                if (!scheduled && !stopped) {
+                    scheduled = requestAnimationFrame(doRender);
+                }
+            },
+            stop: function () {
+                if (scheduled) {
+                    cancelAnimationFrame(scheduled);
+                    scheduled = undefined;
+                }
+                stopped = true;
+            },
+            resume: function () {
+                stopped = false;
+                renderCompleted = true;
+                projector.scheduleRender();
+            },
+            append: function (parentNode, renderMaquetteFunction) {
+                projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions));
+                renderFunctions.push(renderMaquetteFunction);
+            },
+            insertBefore: function (beforeNode, renderMaquetteFunction) {
+                projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions));
+                renderFunctions.push(renderMaquetteFunction);
+            },
+            merge: function (domNode, renderMaquetteFunction) {
+                projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions));
+                renderFunctions.push(renderMaquetteFunction);
+            },
+            replace: function (domNode, renderMaquetteFunction) {
+                var vnode = renderMaquetteFunction();
+                createDom(vnode, domNode.parentNode, domNode, projectionOptions);
+                domNode.parentNode.removeChild(domNode);
+                projections.push(createProjection(vnode, projectionOptions));
+                renderFunctions.push(renderMaquetteFunction);
+            },
+            detach: function (renderMaquetteFunction) {
+                for (var i = 0; i < renderFunctions.length; i++) {
+                    if (renderFunctions[i] === renderMaquetteFunction) {
+                        renderFunctions.splice(i, 1);
+                        return projections.splice(i, 1)[0];
+                    }
+                }
+                throw new Error('renderMaquetteFunction was not found');
+            }
+        };
+        return projector;
+    };
+}));

+ 3 - 0
plugins/UiConfig/media/js/utils/Dollar.coffee

@@ -0,0 +1,3 @@
+window.$ = (selector) ->
+	if selector.startsWith("#")
+		return document.getElementById(selector.replace("#", ""))

+ 85 - 0
plugins/UiConfig/media/js/utils/ZeroFrame.coffee

@@ -0,0 +1,85 @@
+class ZeroFrame extends Class
+	constructor: (url) ->
+		@url = url
+		@waiting_cb = {}
+		@wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1")
+		@connect()
+		@next_message_id = 1
+		@history_state = {}
+		@init()
+
+
+	init: ->
+		@
+
+
+	connect: ->
+		@target = window.parent
+		window.addEventListener("message", @onMessage, false)
+		@cmd("innerReady")
+
+		# Save scrollTop
+		window.addEventListener "beforeunload", (e) =>
+			@log "save scrollTop", window.pageYOffset
+			@history_state["scrollTop"] = window.pageYOffset
+			@cmd "wrapperReplaceState", [@history_state, null]
+
+		# Restore scrollTop
+		@cmd "wrapperGetState", [], (state) =>
+			@history_state = state if state?
+			@log "restore scrollTop", state, window.pageYOffset
+			if window.pageYOffset == 0 and state
+				window.scroll(window.pageXOffset, state.scrollTop)
+
+
+	onMessage: (e) =>
+		message = e.data
+		cmd = message.cmd
+		if cmd == "response"
+			if @waiting_cb[message.to]?
+				@waiting_cb[message.to](message.result)
+			else
+				@log "Websocket callback not found:", message
+		else if cmd == "wrapperReady" # Wrapper inited later
+			@cmd("innerReady")
+		else if cmd == "ping"
+			@response message.id, "pong"
+		else if cmd == "wrapperOpenedWebsocket"
+			@onOpenWebsocket()
+		else if cmd == "wrapperClosedWebsocket"
+			@onCloseWebsocket()
+		else
+			@onRequest cmd, message.params
+
+
+	onRequest: (cmd, message) =>
+		@log "Unknown request", message
+
+
+	response: (to, result) ->
+		@send {"cmd": "response", "to": to, "result": result}
+
+
+	cmd: (cmd, params={}, cb=null) ->
+		@send {"cmd": cmd, "params": params}, cb
+
+
+	send: (message, cb=null) ->
+		message.wrapper_nonce = @wrapper_nonce
+		message.id = @next_message_id
+		@next_message_id += 1
+		@target.postMessage(message, "*")
+		if cb
+			@waiting_cb[message.id] = cb
+
+
+	onOpenWebsocket: =>
+		@log "Websocket open"
+
+
+	onCloseWebsocket: =>
+		@log "Websocket close"
+
+
+
+window.ZeroFrame = ZeroFrame

Some files were not shown because too many files changed in this diff