UiConfig plugin

This commit is contained in:
shortcutme 2018-07-10 03:30:47 +02:00
parent 5aab10fab2
commit 10bab2b6e4
No known key found for this signature in database
GPG key ID: 5B63BAE6CB9613AE
17 changed files with 3240 additions and 0 deletions

View file

@ -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

View file

@ -0,0 +1 @@
import UiConfigPlugin

View file

@ -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>

View file

@ -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 one or more lines are too long

View file

@ -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 one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

View file

@ -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

View file

@ -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()

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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
###

View file

@ -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

View file

@ -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;
};
}));

View file

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

View file

@ -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