Plugin to install, update and delete third-party plugins using the web interface
This commit is contained in:
parent
0877fec638
commit
4094d3a9bf
18 changed files with 3396 additions and 0 deletions
220
plugins/UiPluginManager/UiPluginManagerPlugin.py
Normal file
220
plugins/UiPluginManager/UiPluginManagerPlugin.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
import io
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from Plugin import PluginManager
|
||||
from Config import config
|
||||
from Debug import Debug
|
||||
from Translate import Translate
|
||||
|
||||
|
||||
plugin_dir = os.path.dirname(__file__)
|
||||
|
||||
if "_" not in locals():
|
||||
_ = Translate(plugin_dir + "/languages/")
|
||||
|
||||
|
||||
@PluginManager.afterLoad
|
||||
def importPluginnedClasses():
|
||||
from Ui import UiWebsocket
|
||||
UiWebsocket.admin_commands.update([
|
||||
"pluginList", "pluginConfigSet", "pluginAdd", "pluginRemove", "pluginUpdate"
|
||||
])
|
||||
|
||||
|
||||
# Convert non-str,int,float values to str in a dict
|
||||
def restrictDictValues(input_dict):
|
||||
allowed_types = (int, str, float)
|
||||
return {
|
||||
key: val if type(val) in allowed_types else str(val)
|
||||
for key, val in input_dict.items()
|
||||
}
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiRequest")
|
||||
class UiRequestPlugin(object):
|
||||
def actionWrapper(self, path, extra_headers=None):
|
||||
if path.strip("/") != "Plugins":
|
||||
return super(UiRequestPlugin, self).actionWrapper(path, extra_headers)
|
||||
|
||||
if not extra_headers:
|
||||
extra_headers = {}
|
||||
|
||||
script_nonce = self.getScriptNonce()
|
||||
|
||||
self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce)
|
||||
site = self.server.site_manager.get(config.homepage)
|
||||
return iter([super(UiRequestPlugin, self).renderWrapper(
|
||||
site, path, "uimedia/plugins/plugin_manager/plugin_manager.html",
|
||||
"Plugin Manager", extra_headers, show_loadingscreen=False, script_nonce=script_nonce
|
||||
)])
|
||||
|
||||
def actionUiMedia(self, path, *args, **kwargs):
|
||||
if path.startswith("/uimedia/plugins/plugin_manager/"):
|
||||
file_path = path.replace("/uimedia/plugins/plugin_manager/", plugin_dir + "/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)
|
||||
|
||||
if file_path.endswith("js"):
|
||||
data = _.translateData(open(file_path).read(), mode="js").encode("utf8")
|
||||
elif file_path.endswith("html"):
|
||||
data = _.translateData(open(file_path).read(), mode="html").encode("utf8")
|
||||
else:
|
||||
data = open(file_path, "rb").read()
|
||||
|
||||
return self.actionFile(file_path, file_obj=io.BytesIO(data), file_size=len(data))
|
||||
else:
|
||||
return super(UiRequestPlugin, self).actionUiMedia(path)
|
||||
|
||||
|
||||
@PluginManager.registerTo("UiWebsocket")
|
||||
class UiWebsocketPlugin(object):
|
||||
def actionPluginList(self, to):
|
||||
plugins = []
|
||||
for plugin in PluginManager.plugin_manager.listPlugins(list_disabled=True):
|
||||
plugin_info_path = plugin["dir_path"] + "/plugin_info.json"
|
||||
plugin_info = {}
|
||||
if os.path.isfile(plugin_info_path):
|
||||
try:
|
||||
plugin_info = json.load(open(plugin_info_path))
|
||||
except Exception as err:
|
||||
self.log.error(
|
||||
"Error loading plugin info for %s: %s" %
|
||||
(plugin["name"], Debug.formatException(err))
|
||||
)
|
||||
if plugin_info:
|
||||
plugin_info = restrictDictValues(plugin_info) # For security reasons don't allow complex values
|
||||
plugin["info"] = plugin_info
|
||||
|
||||
if plugin["source"] != "builtin":
|
||||
plugin_site = self.server.sites.get(plugin["source"])
|
||||
if plugin_site:
|
||||
try:
|
||||
plugin_site_info = plugin_site.storage.loadJson(plugin["inner_path"] + "/plugin_info.json")
|
||||
plugin_site_info = restrictDictValues(plugin_site_info)
|
||||
plugin["site_info"] = plugin_site_info
|
||||
plugin["site_title"] = plugin_site.content_manager.contents["content.json"].get("title")
|
||||
plugin_key = "%s/%s" % (plugin["source"], plugin["inner_path"])
|
||||
plugin["updated"] = plugin_key in PluginManager.plugin_manager.plugins_updated
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
plugins.append(plugin)
|
||||
|
||||
return {"plugins": plugins}
|
||||
|
||||
def actionPluginConfigSet(self, to, source, inner_path, key, value):
|
||||
plugin_manager = PluginManager.plugin_manager
|
||||
plugins = plugin_manager.listPlugins(list_disabled=True)
|
||||
plugin = None
|
||||
for item in plugins:
|
||||
if item["source"] == source and item["inner_path"] in (inner_path, "disabled-" + inner_path):
|
||||
plugin = item
|
||||
break
|
||||
|
||||
if not plugin:
|
||||
return {"error": "Plugin not found"}
|
||||
|
||||
config_source = plugin_manager.config.setdefault(source, {})
|
||||
config_plugin = config_source.setdefault(inner_path, {})
|
||||
|
||||
if key in config_plugin and value is None:
|
||||
del config_plugin[key]
|
||||
else:
|
||||
config_plugin[key] = value
|
||||
|
||||
plugin_manager.saveConfig()
|
||||
|
||||
return "ok"
|
||||
|
||||
def pluginAction(self, action, address, inner_path):
|
||||
site = self.server.sites.get(address)
|
||||
plugin_manager = PluginManager.plugin_manager
|
||||
|
||||
# Install/update path should exists
|
||||
if action in ("add", "update", "add_request"):
|
||||
if not site:
|
||||
raise Exception("Site not found")
|
||||
|
||||
if not site.storage.isDir(inner_path):
|
||||
raise Exception("Directory not found on the site")
|
||||
|
||||
try:
|
||||
plugin_info = site.storage.loadJson(inner_path + "/plugin_info.json")
|
||||
plugin_data = (plugin_info["rev"], plugin_info["description"], plugin_info["name"])
|
||||
except Exception as err:
|
||||
raise Exception("Invalid plugin_info.json: %s" % Debug.formatExceptionMessage(err))
|
||||
|
||||
source_path = site.storage.getPath(inner_path)
|
||||
|
||||
target_path = plugin_manager.path_installed_plugins + "/" + address + "/" + inner_path
|
||||
plugin_config = plugin_manager.config.setdefault(site.address, {}).setdefault(inner_path, {})
|
||||
|
||||
# Make sure plugin (not)installed
|
||||
if action in ("add", "add_request") and os.path.isdir(target_path):
|
||||
raise Exception("Plugin already installed")
|
||||
|
||||
if action in ("update", "remove") and not os.path.isdir(target_path):
|
||||
raise Exception("Plugin not installed")
|
||||
|
||||
# Do actions
|
||||
if action == "add":
|
||||
shutil.copytree(source_path, target_path)
|
||||
|
||||
plugin_config["date_added"] = int(time.time())
|
||||
plugin_config["rev"] = plugin_info["rev"]
|
||||
plugin_config["enabled"] = True
|
||||
|
||||
if action == "update":
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
shutil.copytree(source_path, target_path)
|
||||
|
||||
plugin_config["rev"] = plugin_info["rev"]
|
||||
plugin_config["date_updated"] = time.time()
|
||||
|
||||
if action == "remove":
|
||||
del plugin_manager.config[address][inner_path]
|
||||
shutil.rmtree(target_path)
|
||||
|
||||
def doPluginAdd(self, to, inner_path, res):
|
||||
if not res:
|
||||
return None
|
||||
|
||||
self.pluginAction("add", self.site.address, inner_path)
|
||||
PluginManager.plugin_manager.saveConfig()
|
||||
|
||||
self.cmd(
|
||||
"confirm",
|
||||
["Plugin installed!<br>You have to restart the client to load the plugin", "Restart"],
|
||||
lambda res: self.actionServerShutdown(to, restart=True)
|
||||
)
|
||||
|
||||
self.response(to, "ok")
|
||||
|
||||
def actionPluginAddRequest(self, to, inner_path):
|
||||
self.pluginAction("add_request", self.site.address, inner_path)
|
||||
plugin_info = self.site.storage.loadJson(inner_path + "/plugin_info.json")
|
||||
warning = "<b>Warning!<br/>Plugins has the same permissions as the ZeroNet client.<br/>"
|
||||
warning += "Do not install it if you don't trust the developer.</b>"
|
||||
|
||||
self.cmd(
|
||||
"confirm",
|
||||
["Install new plugin: %s?<br>%s" % (plugin_info["name"], warning), "Trust & Install"],
|
||||
lambda res: self.doPluginAdd(to, inner_path, res)
|
||||
)
|
||||
|
||||
def actionPluginRemove(self, to, address, inner_path):
|
||||
self.pluginAction("remove", address, inner_path)
|
||||
PluginManager.plugin_manager.saveConfig()
|
||||
return "ok"
|
||||
|
||||
def actionPluginUpdate(self, to, address, inner_path):
|
||||
self.pluginAction("update", address, inner_path)
|
||||
PluginManager.plugin_manager.saveConfig()
|
||||
PluginManager.plugin_manager.plugins_updated["%s/%s" % (address, inner_path)] = True
|
||||
return "ok"
|
1
plugins/UiPluginManager/__init__.py
Normal file
1
plugins/UiPluginManager/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from . import UiPluginManagerPlugin
|
75
plugins/UiPluginManager/media/css/PluginManager.css
Normal file
75
plugins/UiPluginManager/media/css/PluginManager.css
Normal file
|
@ -0,0 +1,75 @@
|
|||
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 }
|
||||
h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; }
|
||||
h2 { margin-top: 10px; }
|
||||
h3 { font-weight: normal }
|
||||
h4 { font-size: 19px; font-weight: lighter; margin-right: 100px; margin-top: 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; padding-bottom: 150px; }
|
||||
.section { margin: 0px 10%; }
|
||||
.plugins { font-size: 19px; margin-top: 25px; margin-bottom: 75px; }
|
||||
.plugin { transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: relative; padding-bottom: 20px; padding-top: 10px; }
|
||||
.plugin.hidden { opacity: 0; height: 0px; padding: 0px; }
|
||||
.plugin .title { display: inline-block; line-height: 36px; }
|
||||
.plugin .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; }
|
||||
.plugin .title .version { font-size: 70%; margin-left: 5px; }
|
||||
.plugin .title .version .version-latest { color: #2ecc71; font-weight: normal; }
|
||||
.plugin .title .version .version-missing { color: #ffa200; font-weight: normal; }
|
||||
.plugin .title .version .version-update { padding: 0px 15px; margin-left: 5px; line-height: 28px; }
|
||||
.plugin .description { font-size: 14px; color: #666; line-height: 24px; }
|
||||
.plugin .description .source { color: #999; font-size: 90%; }
|
||||
.plugin .description .source a { color: #666; }
|
||||
.plugin .value { display: inline-block; white-space: nowrap; }
|
||||
.plugin .value-right { right: 0px; position: absolute; }
|
||||
.plugin .value-fullwidth { width: 100% }
|
||||
.plugin .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;
|
||||
}
|
||||
.plugin .marker.visible { opacity: 1; pointer-events: all; transform: scale(1); }
|
||||
.plugin .marker.changed { color: #2ecc71; }
|
||||
.plugin .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; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px);
|
||||
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; }
|
||||
|
||||
.animate { transition: all 0.3s ease-out !important; }
|
||||
.animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; }
|
||||
.animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; }
|
129
plugins/UiPluginManager/media/css/all.css
Normal file
129
plugins/UiPluginManager/media/css/all.css
Normal file
File diff suppressed because one or more lines are too long
12
plugins/UiPluginManager/media/css/button.css
Normal file
12
plugins/UiPluginManager/media/css/button.css
Normal 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 }
|
30
plugins/UiPluginManager/media/css/fonts.css
Normal file
30
plugins/UiPluginManager/media/css/fonts.css
Normal file
File diff suppressed because one or more lines are too long
BIN
plugins/UiPluginManager/media/img/loading.gif
Normal file
BIN
plugins/UiPluginManager/media/img/loading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 723 B |
132
plugins/UiPluginManager/media/js/PluginList.coffee
Normal file
132
plugins/UiPluginManager/media/js/PluginList.coffee
Normal file
|
@ -0,0 +1,132 @@
|
|||
class PluginList extends Class
|
||||
constructor: (plugins) ->
|
||||
@plugins = plugins
|
||||
|
||||
savePluginStatus: (plugin, is_enabled) =>
|
||||
Page.cmd "pluginConfigSet", [plugin.source, plugin.inner_path, "enabled", is_enabled], (res) =>
|
||||
if res == "ok"
|
||||
Page.updatePlugins()
|
||||
else
|
||||
Page.cmd "wrapperNotification", ["error", res.error]
|
||||
|
||||
Page.projector.scheduleRender()
|
||||
|
||||
handleCheckboxChange: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
node.classList.toggle("checked")
|
||||
value = node.classList.contains("checked")
|
||||
|
||||
@savePluginStatus(plugin, value)
|
||||
|
||||
handleResetClick: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
|
||||
@savePluginStatus(plugin, null)
|
||||
|
||||
handleUpdateClick: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
node.classList.add("loading")
|
||||
|
||||
Page.cmd "pluginUpdate", [plugin.source, plugin.inner_path], (res) =>
|
||||
if res == "ok"
|
||||
Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} updated to latest version"]
|
||||
Page.updatePlugins()
|
||||
else
|
||||
Page.cmd "wrapperNotification", ["error", res.error]
|
||||
node.classList.remove("loading")
|
||||
|
||||
return false
|
||||
|
||||
handleDeleteClick: (e) =>
|
||||
node = e.currentTarget
|
||||
plugin = node["data-plugin"]
|
||||
if plugin.loaded
|
||||
Page.cmd "wrapperNotification", ["info", "You can only delete plugin that are not currently active"]
|
||||
return false
|
||||
|
||||
node.classList.add("loading")
|
||||
|
||||
Page.cmd "wrapperConfirm", ["Delete #{plugin.name} plugin?", "Delete"], (res) =>
|
||||
if not res
|
||||
node.classList.remove("loading")
|
||||
return false
|
||||
|
||||
Page.cmd "pluginRemove", [plugin.source, plugin.inner_path], (res) =>
|
||||
if res == "ok"
|
||||
Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} deleted"]
|
||||
Page.updatePlugins()
|
||||
else
|
||||
Page.cmd "wrapperNotification", ["error", res.error]
|
||||
node.classList.remove("loading")
|
||||
|
||||
return false
|
||||
|
||||
render: ->
|
||||
h("div.plugins", @plugins.map (plugin) =>
|
||||
if not plugin.info
|
||||
return
|
||||
descr = plugin.info.description
|
||||
plugin.info.default ?= "enabled"
|
||||
if plugin.info.default
|
||||
descr += " (default: #{plugin.info.default})"
|
||||
|
||||
tag_version = ""
|
||||
tag_source = ""
|
||||
tag_delete = ""
|
||||
if plugin.source != "builtin"
|
||||
tag_update = ""
|
||||
if plugin.site_info?.rev
|
||||
if plugin.site_info.rev > plugin.info.rev
|
||||
tag_update = h("a.version-update.button",
|
||||
{href: "#Update+plugin", onclick: @handleUpdateClick, "data-plugin": plugin},
|
||||
"Update to rev#{plugin.site_info.rev}"
|
||||
)
|
||||
|
||||
else
|
||||
tag_update = h("span.version-missing", "(unable to get latest vesion: update site missing)")
|
||||
|
||||
tag_version = h("span.version",[
|
||||
"rev#{plugin.info.rev} ",
|
||||
tag_update,
|
||||
])
|
||||
|
||||
tag_source = h("div.source",[
|
||||
"Source: ",
|
||||
h("a", {"href": "/#{plugin.source}", "target": "_top"}, if plugin.site_title then plugin.site_title else plugin.source),
|
||||
" /" + plugin.inner_path
|
||||
])
|
||||
|
||||
tag_delete = h("a.delete", {"href": "#Delete+plugin", onclick: @handleDeleteClick, "data-plugin": plugin}, "Delete plugin")
|
||||
|
||||
|
||||
enabled_default = plugin.info.default == "enabled"
|
||||
if plugin.enabled != plugin.loaded or plugin.updated
|
||||
marker_title = "Change pending"
|
||||
is_pending = true
|
||||
else
|
||||
marker_title = "Changed from default status (click to reset to #{plugin.info.default})"
|
||||
is_pending = false
|
||||
|
||||
is_changed = plugin.enabled != enabled_default and plugin.owner == "builtin"
|
||||
|
||||
h("div.plugin", {key: plugin.name}, [
|
||||
h("div.title", [
|
||||
h("h3", [plugin.name, tag_version]),
|
||||
h("div.description", [descr, tag_source, tag_delete]),
|
||||
])
|
||||
h("div.value.value-right",
|
||||
h("div.checkbox", {onclick: @handleCheckboxChange, "data-plugin": plugin, classes: {checked: plugin.enabled}}, h("div.checkbox-skin"))
|
||||
h("a.marker", {
|
||||
href: "#Reset", title: marker_title,
|
||||
onclick: @handleResetClick, "data-plugin": plugin,
|
||||
classes: {visible: is_pending or is_changed, pending: is_pending}
|
||||
}, "\u2022")
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
|
||||
window.PluginList = PluginList
|
71
plugins/UiPluginManager/media/js/UiPluginManager.coffee
Normal file
71
plugins/UiPluginManager/media/js/UiPluginManager.coffee
Normal file
|
@ -0,0 +1,71 @@
|
|||
window.h = maquette.h
|
||||
|
||||
class UiPluginManager extends ZeroFrame
|
||||
init: ->
|
||||
@plugin_list_builtin = new PluginList()
|
||||
@plugin_list_custom = new PluginList()
|
||||
@plugins_changed = null
|
||||
@need_restart = null
|
||||
@
|
||||
|
||||
onOpenWebsocket: =>
|
||||
@cmd("wrapperSetTitle", "Plugin manager - ZeroNet")
|
||||
@cmd "serverInfo", {}, (server_info) =>
|
||||
@server_info = server_info
|
||||
@updatePlugins()
|
||||
|
||||
updatePlugins: (cb) =>
|
||||
@cmd "pluginList", [], (res) =>
|
||||
@plugins_changed = (item for item in res.plugins when item.enabled != item.loaded or item.updated)
|
||||
|
||||
plugins_builtin = (item for item in res.plugins when item.source == "builtin")
|
||||
@plugin_list_builtin.plugins = plugins_builtin.sort (a, b) ->
|
||||
return a.name.localeCompare(b.name)
|
||||
|
||||
plugins_custom = (item for item in res.plugins when item.source != "builtin")
|
||||
@plugin_list_custom.plugins = plugins_custom.sort (a, b) ->
|
||||
return a.name.localeCompare(b.name)
|
||||
|
||||
@projector.scheduleRender()
|
||||
cb?()
|
||||
|
||||
createProjector: =>
|
||||
@projector = maquette.createProjector()
|
||||
@projector.replace($("#content"), @render)
|
||||
@projector.replace($("#bottom-restart"), @renderBottomRestart)
|
||||
|
||||
render: =>
|
||||
if not @plugin_list_builtin.plugins
|
||||
return h("div.content")
|
||||
|
||||
h("div.content", [
|
||||
h("div.section", [
|
||||
if @plugin_list_custom.plugins?.length
|
||||
[
|
||||
h("h2", "Installed third-party plugins"),
|
||||
@plugin_list_custom.render()
|
||||
]
|
||||
h("h2", "Built-in plugins")
|
||||
@plugin_list_builtin.render()
|
||||
])
|
||||
])
|
||||
|
||||
handleRestartClick: =>
|
||||
@restart_loading = true
|
||||
setTimeout ( =>
|
||||
Page.cmd("serverShutdown", {restart: true})
|
||||
), 300
|
||||
Page.projector.scheduleRender()
|
||||
return false
|
||||
|
||||
renderBottomRestart: =>
|
||||
h("div.bottom.bottom-restart", {classes: {visible: @plugins_changed?.length}}, h("div.bottom-content", [
|
||||
h("div.title", "Some plugins status has been changed"),
|
||||
h("a.button.button-submit.button-restart",
|
||||
{href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick},
|
||||
"Restart ZeroNet client"
|
||||
)
|
||||
]))
|
||||
|
||||
window.Page = new UiPluginManager()
|
||||
window.Page.createProjector()
|
1606
plugins/UiPluginManager/media/js/all.js
Normal file
1606
plugins/UiPluginManager/media/js/all.js
Normal file
File diff suppressed because it is too large
Load diff
23
plugins/UiPluginManager/media/js/lib/Class.coffee
Normal file
23
plugins/UiPluginManager/media/js/lib/Class.coffee
Normal 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
|
74
plugins/UiPluginManager/media/js/lib/Promise.coffee
Normal file
74
plugins/UiPluginManager/media/js/lib/Promise.coffee
Normal 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
|
||||
###
|
8
plugins/UiPluginManager/media/js/lib/Prototypes.coffee
Normal file
8
plugins/UiPluginManager/media/js/lib/Prototypes.coffee
Normal 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
|
770
plugins/UiPluginManager/media/js/lib/maquette.js
Normal file
770
plugins/UiPluginManager/media/js/lib/maquette.js
Normal 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;
|
||||
};
|
||||
}));
|
138
plugins/UiPluginManager/media/js/utils/Animation.coffee
Normal file
138
plugins/UiPluginManager/media/js/utils/Animation.coffee
Normal file
|
@ -0,0 +1,138 @@
|
|||
class Animation
|
||||
slideDown: (elem, props) ->
|
||||
if elem.offsetTop > 2000
|
||||
return
|
||||
|
||||
h = elem.offsetHeight
|
||||
cstyle = window.getComputedStyle(elem)
|
||||
margin_top = cstyle.marginTop
|
||||
margin_bottom = cstyle.marginBottom
|
||||
padding_top = cstyle.paddingTop
|
||||
padding_bottom = cstyle.paddingBottom
|
||||
transition = cstyle.transition
|
||||
|
||||
elem.style.boxSizing = "border-box"
|
||||
elem.style.overflow = "hidden"
|
||||
elem.style.transform = "scale(0.6)"
|
||||
elem.style.opacity = "0"
|
||||
elem.style.height = "0px"
|
||||
elem.style.marginTop = "0px"
|
||||
elem.style.marginBottom = "0px"
|
||||
elem.style.paddingTop = "0px"
|
||||
elem.style.paddingBottom = "0px"
|
||||
elem.style.transition = "none"
|
||||
|
||||
setTimeout (->
|
||||
elem.className += " animate-inout"
|
||||
elem.style.height = h+"px"
|
||||
elem.style.transform = "scale(1)"
|
||||
elem.style.opacity = "1"
|
||||
elem.style.marginTop = margin_top
|
||||
elem.style.marginBottom = margin_bottom
|
||||
elem.style.paddingTop = padding_top
|
||||
elem.style.paddingBottom = padding_bottom
|
||||
), 1
|
||||
|
||||
elem.addEventListener "transitionend", ->
|
||||
elem.classList.remove("animate-inout")
|
||||
elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null
|
||||
elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null
|
||||
elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null
|
||||
elem.removeEventListener "transitionend", arguments.callee, false
|
||||
|
||||
|
||||
slideUp: (elem, remove_func, props) ->
|
||||
if elem.offsetTop > 1000
|
||||
return remove_func()
|
||||
|
||||
elem.className += " animate-back"
|
||||
elem.style.boxSizing = "border-box"
|
||||
elem.style.height = elem.offsetHeight+"px"
|
||||
elem.style.overflow = "hidden"
|
||||
elem.style.transform = "scale(1)"
|
||||
elem.style.opacity = "1"
|
||||
elem.style.pointerEvents = "none"
|
||||
setTimeout (->
|
||||
elem.style.height = "0px"
|
||||
elem.style.marginTop = "0px"
|
||||
elem.style.marginBottom = "0px"
|
||||
elem.style.paddingTop = "0px"
|
||||
elem.style.paddingBottom = "0px"
|
||||
elem.style.transform = "scale(0.8)"
|
||||
elem.style.borderTopWidth = "0px"
|
||||
elem.style.borderBottomWidth = "0px"
|
||||
elem.style.opacity = "0"
|
||||
), 1
|
||||
elem.addEventListener "transitionend", (e) ->
|
||||
if e.propertyName == "opacity" or e.elapsedTime >= 0.6
|
||||
elem.removeEventListener "transitionend", arguments.callee, false
|
||||
remove_func()
|
||||
|
||||
|
||||
slideUpInout: (elem, remove_func, props) ->
|
||||
elem.className += " animate-inout"
|
||||
elem.style.boxSizing = "border-box"
|
||||
elem.style.height = elem.offsetHeight+"px"
|
||||
elem.style.overflow = "hidden"
|
||||
elem.style.transform = "scale(1)"
|
||||
elem.style.opacity = "1"
|
||||
elem.style.pointerEvents = "none"
|
||||
setTimeout (->
|
||||
elem.style.height = "0px"
|
||||
elem.style.marginTop = "0px"
|
||||
elem.style.marginBottom = "0px"
|
||||
elem.style.paddingTop = "0px"
|
||||
elem.style.paddingBottom = "0px"
|
||||
elem.style.transform = "scale(0.8)"
|
||||
elem.style.borderTopWidth = "0px"
|
||||
elem.style.borderBottomWidth = "0px"
|
||||
elem.style.opacity = "0"
|
||||
), 1
|
||||
elem.addEventListener "transitionend", (e) ->
|
||||
if e.propertyName == "opacity" or e.elapsedTime >= 0.6
|
||||
elem.removeEventListener "transitionend", arguments.callee, false
|
||||
remove_func()
|
||||
|
||||
|
||||
showRight: (elem, props) ->
|
||||
elem.className += " animate"
|
||||
elem.style.opacity = 0
|
||||
elem.style.transform = "TranslateX(-20px) Scale(1.01)"
|
||||
setTimeout (->
|
||||
elem.style.opacity = 1
|
||||
elem.style.transform = "TranslateX(0px) Scale(1)"
|
||||
), 1
|
||||
elem.addEventListener "transitionend", ->
|
||||
elem.classList.remove("animate")
|
||||
elem.style.transform = elem.style.opacity = null
|
||||
|
||||
|
||||
show: (elem, props) ->
|
||||
delay = arguments[arguments.length-2]?.delay*1000 or 1
|
||||
elem.style.opacity = 0
|
||||
setTimeout (->
|
||||
elem.className += " animate"
|
||||
), 1
|
||||
setTimeout (->
|
||||
elem.style.opacity = 1
|
||||
), delay
|
||||
elem.addEventListener "transitionend", ->
|
||||
elem.classList.remove("animate")
|
||||
elem.style.opacity = null
|
||||
elem.removeEventListener "transitionend", arguments.callee, false
|
||||
|
||||
hide: (elem, remove_func, props) ->
|
||||
delay = arguments[arguments.length-2]?.delay*1000 or 1
|
||||
elem.className += " animate"
|
||||
setTimeout (->
|
||||
elem.style.opacity = 0
|
||||
), delay
|
||||
elem.addEventListener "transitionend", (e) ->
|
||||
if e.propertyName == "opacity"
|
||||
remove_func()
|
||||
|
||||
addVisibleClass: (elem, props) ->
|
||||
setTimeout ->
|
||||
elem.classList.add("visible")
|
||||
|
||||
window.Animation = new Animation()
|
3
plugins/UiPluginManager/media/js/utils/Dollar.coffee
Normal file
3
plugins/UiPluginManager/media/js/utils/Dollar.coffee
Normal file
|
@ -0,0 +1,3 @@
|
|||
window.$ = (selector) ->
|
||||
if selector.startsWith("#")
|
||||
return document.getElementById(selector.replace("#", ""))
|
85
plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee
Normal file
85
plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee
Normal 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
|
19
plugins/UiPluginManager/media/plugin_manager.html
Normal file
19
plugins/UiPluginManager/media/plugin_manager.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<!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 plugin manager</h1>
|
||||
|
||||
<div class="content" id="content"></div>
|
||||
<div class="bottom" id="bottom-restart"></div>
|
||||
|
||||
<script type="text/javascript" src="js/all.js?rev={rev}&lang={lang}"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue