UiFileManager plugin
This commit is contained in:
parent
85790f8866
commit
f0b0f57643
24 changed files with 2541 additions and 0 deletions
84
plugins/UiFileManager/UiFileManagerPlugin.py
Normal file
84
plugins/UiFileManager/UiFileManagerPlugin.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
from Plugin import PluginManager
|
||||||
|
from Config import config
|
||||||
|
from Translate import Translate
|
||||||
|
|
||||||
|
plugin_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
if "_" not in locals():
|
||||||
|
_ = Translate(plugin_dir + "/languages/")
|
||||||
|
|
||||||
|
|
||||||
|
@PluginManager.registerTo("UiRequest")
|
||||||
|
class UiFileManagerPlugin(object):
|
||||||
|
def actionWrapper(self, path, extra_headers=None):
|
||||||
|
match = re.match("/list/(.*?)(/.*|)$", path)
|
||||||
|
if not match:
|
||||||
|
return super().actionWrapper(path, extra_headers)
|
||||||
|
|
||||||
|
if not extra_headers:
|
||||||
|
extra_headers = {}
|
||||||
|
|
||||||
|
request_address, inner_path = match.groups()
|
||||||
|
|
||||||
|
script_nonce = self.getScriptNonce()
|
||||||
|
|
||||||
|
self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce)
|
||||||
|
|
||||||
|
site = self.server.site_manager.need(request_address)
|
||||||
|
|
||||||
|
if not site:
|
||||||
|
return super().actionWrapper(path, extra_headers)
|
||||||
|
|
||||||
|
request_params = urllib.parse.urlencode(
|
||||||
|
{"address": site.address, "site": request_address, "inner_path": inner_path.strip("/")}
|
||||||
|
)
|
||||||
|
|
||||||
|
is_content_loaded = "content.json" in site.content_manager.contents
|
||||||
|
|
||||||
|
return iter([super().renderWrapper(
|
||||||
|
site, path, "uimedia/plugins/uifilemanager/list.html?%s" % request_params,
|
||||||
|
"List", extra_headers, show_loadingscreen=not is_content_loaded, script_nonce=script_nonce
|
||||||
|
)])
|
||||||
|
|
||||||
|
def actionUiMedia(self, path, *args, **kwargs):
|
||||||
|
if path.startswith("/uimedia/plugins/uifilemanager/"):
|
||||||
|
file_path = path.replace("/uimedia/plugins/uifilemanager/", 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"):
|
||||||
|
if self.get.get("address"):
|
||||||
|
site = self.server.site_manager.need(self.get.get("address"))
|
||||||
|
if "content.json" not in site.content_manager.contents:
|
||||||
|
site.needFile("content.json")
|
||||||
|
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().actionUiMedia(path)
|
||||||
|
|
||||||
|
def error404(self, path=""):
|
||||||
|
if not path.endswith("index.html") and not path.endswith("/"):
|
||||||
|
return super().error404(path)
|
||||||
|
|
||||||
|
path_parts = self.parsePath(path)
|
||||||
|
site = self.server.site_manager.get(path_parts["request_address"])
|
||||||
|
|
||||||
|
if not site or not site.content_manager.contents.get("content.json"):
|
||||||
|
return super().error404(path)
|
||||||
|
|
||||||
|
self.sendHeader(200)
|
||||||
|
path_redirect = "/list" + re.sub("^/media/", "/", path)
|
||||||
|
self.log.debug("Index.html not found: %s, redirecting to: %s" % (path, path_redirect))
|
||||||
|
return self.formatRedirect(path_redirect)
|
1
plugins/UiFileManager/__init__.py
Normal file
1
plugins/UiFileManager/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from . import UiFileManagerPlugin
|
20
plugins/UiFileManager/languages/hu.json
Normal file
20
plugins/UiFileManager/languages/hu.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"New file name:": "Új fájl neve:",
|
||||||
|
"Delete": "Törlés",
|
||||||
|
"Cancel": "Mégse",
|
||||||
|
"Selected:": "Köjelölt:",
|
||||||
|
"Delete and remove optional:": "Törlés és opcionális fájl eltávolítása",
|
||||||
|
" files": " fájl",
|
||||||
|
" (modified)": " (módostott)",
|
||||||
|
" (new)": " (új)",
|
||||||
|
" (optional)": " (opcionális)",
|
||||||
|
" (ignored from content.json)": " (content.json-ból kihagyott)",
|
||||||
|
"Total: ": "Összesen: ",
|
||||||
|
" dir, ": " könyvtár, ",
|
||||||
|
" file in ": " fájl, ",
|
||||||
|
"+ New": "+ Új",
|
||||||
|
"Edit": "Módosít",
|
||||||
|
"View": "Megnyit",
|
||||||
|
"Save": "Mentés",
|
||||||
|
"Save: done!": "Mentés: Kész!"
|
||||||
|
}
|
33
plugins/UiFileManager/media/css/Menu.css
Normal file
33
plugins/UiFileManager/media/css/Menu.css
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
.menu {
|
||||||
|
background-color: white; padding: 10px 0px; position: absolute; top: 0px; max-height: 0px; overflow: hidden; transform: translate(-100%, -30px); pointer-events: none;
|
||||||
|
box-shadow: 0px 2px 8px rgba(0,0,0,0.3); border-radius: 2px; opacity: 0; transition: opacity 0.2s ease-out, transform 1s ease-out, max-height 0.2s ease-in-out; z-index: 99;
|
||||||
|
display: inline-block; z-index: 999; transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
.menu.menu-left { transform: translate(0%, -30px); }
|
||||||
|
.menu.menu-left.visible { transform: translate(0%, 0px); }
|
||||||
|
.menu.visible {
|
||||||
|
opacity: 1; transform: translate(-100%, 0px); pointer-events: all;
|
||||||
|
transition: opacity 0.1s ease-out, transform 0.3s ease-out, max-height 0.3s cubic-bezier(0.86, 0, 0.07, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: block; text-decoration: none; color: black; padding: 6px 24px; transition: all 0.2s; border-bottom: none; font-weight: normal;
|
||||||
|
max-height: 150px; overflow: hidden; text-overflow: ellipsis; -webkit-line-clamp: 6; -webkit-box-orient: vertical; display: -webkit-box;
|
||||||
|
}
|
||||||
|
.menu-item-separator { margin-top: 3px; margin-bottom: 3px; border-top: 1px solid #eee }
|
||||||
|
|
||||||
|
.menu-item.noaction { cursor: default }
|
||||||
|
.menu-item:hover:not(.noaction) { background-color: #F6F6F6; transition: none; color: inherit; cursor: pointer; color: black }
|
||||||
|
.menu-item:active:not(.noaction), .menu-item:focus:not(.noaction) { background-color: #AF3BFF !important; color: white !important; transition: none }
|
||||||
|
.menu-item.selected:before {
|
||||||
|
content: "L"; display: inline-block; transform: rotateZ(45deg) scaleX(-1);
|
||||||
|
font-weight: bold; position: absolute; margin-left: -14px; font-size: 12px; margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-radio { white-space: normal; line-height: 26px }
|
||||||
|
.menu-radio a {
|
||||||
|
background-color: #EEE; width: 18.5%;; text-align: center; margin-top: 2px; margin-bottom: 2px; color: #666; font-weight: bold;
|
||||||
|
text-decoration: none; font-size: 13px; transition: all 0.3s; text-transform: uppercase; display: inline-block;
|
||||||
|
}
|
||||||
|
.menu-radio a:hover, .menu-radio a.selected { transition: none; background-color: #AF3BFF !important; color: white !important }
|
||||||
|
.menu-radio a.long { font-size: 10px; vertical-align: -1px; }
|
17
plugins/UiFileManager/media/css/Selectbar.css
Normal file
17
plugins/UiFileManager/media/css/Selectbar.css
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.selectbar.visible { margin-top: 0px; visibility: visible }
|
||||||
|
.selectbar {
|
||||||
|
position: fixed; top: 0; left: 0; background-color: white; box-shadow: 0px 0px 25px rgba(22, 39, 97, 0.2); margin-top: -75px;
|
||||||
|
transition: all 0.3s; visibility: hidden; z-index: 9999; color: black; border-left: 5px solid #ede1f582; width: 100%;
|
||||||
|
padding: 13px; font-size: 13px; font-weight: lighter; backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectbar .num { margin-left: 15px; min-width: 30px; text-align: right; display: inline-block; }
|
||||||
|
.selectbar .size { margin-left: 10px; color: #9f9ba2; min-width: 75px; display: inline-block; }
|
||||||
|
.selectbar .actions { display: inline-block; margin-left: 20px; font-size: 13px; text-transform: uppercase; line-height: 20px; }
|
||||||
|
.selectbar .action { padding: 5px 20px; border: 1px solid #edd4ff; margin-left: 10px; border-radius: 30px; color: #af3bff; text-decoration: none; transition: all 0.3s }
|
||||||
|
.selectbar .action:hover { border-color: #c788f3; transition: none; color: #9700ff }
|
||||||
|
.selectbar .delete { color: #AAA; border-color: #DDD; }
|
||||||
|
.selectbar .delete:hover { color: #333; border-color: #AAA }
|
||||||
|
.selectbar .action:active { background-color: #af3bff; color: white; border-color: #af3bff; transition: none }
|
||||||
|
.selectbar .cancel { margin: 20px; font-size: 10px; text-decoration: none; color: #999; text-transform: uppercase; }
|
||||||
|
.selectbar .cancel:hover { color: #333; transition: none }
|
148
plugins/UiFileManager/media/css/UiFileManager.css
Normal file
148
plugins/UiFileManager/media/css/UiFileManager.css
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
body { background-color: #EEEEF5; font-family: "Segoe UI", Helvetica, Arial; height: 95000px; overflow: hidden; }
|
||||||
|
body.loaded { height: auto; overflow: auto }
|
||||||
|
h1 { font-weight: lighter; }
|
||||||
|
|
||||||
|
a { color: #333 }
|
||||||
|
a:hover { text-decoration: none }
|
||||||
|
input::placeholder { color: rgba(255, 255, 255, 0.3) }
|
||||||
|
|
||||||
|
h2 { font-weight: lighter; }
|
||||||
|
|
||||||
|
.link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s }
|
||||||
|
.link:active { background-color: #fbf5ff; outline: 5px solid #fbf5ff; transition: none }
|
||||||
|
|
||||||
|
.manager.editing .files { float: left; width: 280px; }
|
||||||
|
|
||||||
|
.sidebar-button {
|
||||||
|
display: inline-block; padding: 25px 19px; text-decoration: none; position: absolute;
|
||||||
|
border-right: 1px solid #EEE; line-height: 10px; color: #7801F5; transition: all 0.3s
|
||||||
|
}
|
||||||
|
.sidebar-button:active { background-color: #f5e7ff; transition: none }
|
||||||
|
/*.sidebar-button:hover { background-color: #fbf5ff; }*/
|
||||||
|
.sidebar-button span { transition: 1s all; transform-origin: 2.5px 7px; display: inline-block; }
|
||||||
|
.manager.sidebar_closed .sidebar-button span { transform: rotateZ(180deg); }
|
||||||
|
.manager.sidebar_closed .files { margin-left: -300px; }
|
||||||
|
.manager.sidebar_closed .editor { width: 100%; }
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 5px 10px; margin-left: 10px; background-color: #752ff2; border-bottom: 2px solid #caadff; background-position: -50px center;
|
||||||
|
border-radius: 2px; text-decoration: none; transition: all 0.5s ease-out; display: inline-block;
|
||||||
|
color: #333; font-size: 12px; vertical-align: 2px; text-transform: uppercase; color: white; max-width: 100px;
|
||||||
|
}
|
||||||
|
.button:hover { background-color: #9e71ed; transition: none; }
|
||||||
|
.button:active { position: relative; top: 1px }
|
||||||
|
.button.loading, .button.disabled { color: rgba(255,255,255,0.7);; pointer-events: none; border-bottom: 2px solid #666; background-color: #999; }
|
||||||
|
.button.loading { background: #999 url(../img/loading.gif) no-repeat center center; transition: all 0.5s ease-out; color: rgba(0,0,0,0); }
|
||||||
|
.button.done { background-color: #4dc758; transition: all 0.3s; border-color: #4dc758; pointer-events: none; }
|
||||||
|
.button.hidden { max-width: 0px; display: inline-block; padding-left: 0px; padding-right: 0px; margin: 0px; }
|
||||||
|
|
||||||
|
/* List */
|
||||||
|
|
||||||
|
.files {
|
||||||
|
width: 97%; box-sizing: border-box; color: #555; position: relative; z-index: 1; transition: all 0.6s;
|
||||||
|
font-size: 14px; box-shadow: 0px 9px 20px -15px #a5cbec; max-width: 400px; border: 1px solid #EEEEF5;
|
||||||
|
}
|
||||||
|
.files .tr { white-space: nowrap }
|
||||||
|
.files .td { display: inline-block; width: 60px }
|
||||||
|
.files .tbody .td { line-height: 18px; vertical-align: bottom; }
|
||||||
|
.files .td.name { min-width: 100px }
|
||||||
|
.files .td.size { width: 60px; text-align: right; padding-left: 5px; }
|
||||||
|
.files .td.status { text-align: right; }
|
||||||
|
.files .td.peer { width: 60px }
|
||||||
|
.files .td.uploaded { width: 130px; text-align: right; }
|
||||||
|
.files .td.added { width: 90px }
|
||||||
|
.files .orderby { color: inherit; text-decoration: none; transition: all 0.3s; outline: 5px solid transparent; }
|
||||||
|
.files .orderby:hover { text-decoration: underline; }
|
||||||
|
.files .orderby .icon-arrow-down { opacity: 0; transition: all 0.3s ease-in-out; }
|
||||||
|
.files .orderby.selected .icon-arrow-down { opacity: 0.3; }
|
||||||
|
.files .orderby:active { background-color: rgba(133, 239, 255, 0.09); outline: 5px solid rgba(133, 239, 255, 0.09); transition: none; }
|
||||||
|
.files .orderby:hover .icon-arrow-down { opacity: 0.5; }
|
||||||
|
.files .orderby:not(.desc) .icon-arrow-down { transform: rotateZ(180deg); }
|
||||||
|
.files .tr.editing .td { background-color: #ede1f582; border-top-color: #ece9ef; }
|
||||||
|
.files .thead { /*background: linear-gradient(358deg, #e7f1f7, #e9f2f72e);*/ }
|
||||||
|
.files .thead .td {
|
||||||
|
border-top: none; color: #8984c2; background-color: #f7f7fc;
|
||||||
|
font-size: 12px; /*text-transform: uppercase; background-color: transparent; font-weight: bold;*/
|
||||||
|
}
|
||||||
|
.files .thead .td a:last-of-type { font-weight: bold; }
|
||||||
|
.files .thead .td a { text-decoration: none; }
|
||||||
|
.files .thead .td a:hover { text-decoration: underline; }
|
||||||
|
.files .tbody { max-height: calc(100vh - 100x); overflow-y: auto; overflow-x: hidden; }
|
||||||
|
.files .tr { background-color: white; }
|
||||||
|
.files .td { padding: 10px 20px; border-top: 1px solid #EEE; font-size: 13px; white-space: nowrap; }
|
||||||
|
.files .td.full { width: 100%; box-sizing: border-box; white-space: pre-line; }
|
||||||
|
.files .td.pre { width: 0px; color: transparent; padding-left: 0px; border-left: 2px solid transparent; }
|
||||||
|
.files .tbody .td { height: 18px; }
|
||||||
|
.files .tbody .td.full { height: auto; }
|
||||||
|
.files .td.pre .checkbox-outer { opacity: 0.6; margin-left: -11px; margin-top: -15px; width: 18px; height: 12px; display: inline-block; }
|
||||||
|
.files .tr.modified .td.pre { border-left-color: #7801F5 }
|
||||||
|
.files .tr.added .td.pre { border-left-color: #00ec93 }
|
||||||
|
.files .tr.ignored .td.pre { border-left-color: #999; }
|
||||||
|
.files .tr.ignored { opacity: 0.5; }
|
||||||
|
.files .tr.optional { background: linear-gradient(90deg, #fff6dd, 30%, white, 10%, white); }
|
||||||
|
.files .tr.optional_empty { color: #999; font-style: italic; }
|
||||||
|
.files .td.error { background-color: #F44336; color: white; }
|
||||||
|
.files .td.site { width: 70px }
|
||||||
|
.files .td.site .link { color: inherit; text-decoration: none }
|
||||||
|
.files .td.status .percent {
|
||||||
|
transition: all 1s ease-in-out; display: inline-block; width: 80px; background-color: #EEE; font-size: 10px;
|
||||||
|
height: 15px; line-height: 15px; text-align: center; margin-right: 20px;
|
||||||
|
}
|
||||||
|
.files .td.name { padding-left: 10px; width: calc(100% - 167px); max-height: 18px; padding-right: 10px; }
|
||||||
|
.files .tr.nobuttons .td.name { width: calc(100% - 127px); }
|
||||||
|
.files .tr.nobuttons .td.buttons { width: 0px; }
|
||||||
|
.files .td.name .title { color: inherit; text-decoration: none }
|
||||||
|
.files .td.name .link { display: inline-block; overflow: hidden; text-overflow: ellipsis; vertical-align: -4px; max-width: 100%; }
|
||||||
|
.files .pinned .td.name .link { max-width: calc(100% - 40px); }
|
||||||
|
.files .thead .td.uploaded { text-align: left }
|
||||||
|
.files .thead .td.uploaded .title { padding-left: 7px; }
|
||||||
|
.files .peer .icon-profile { background: currentColor; color: #47d094; font-size: 10px; top: 1px; margin-right: 13px }
|
||||||
|
.files .peer .icon-profile:before { background: currentColor }
|
||||||
|
.files .peer .num { color: #969696; }
|
||||||
|
.files .uploaded .uploaded-text { display: inline-block; text-align: right; }
|
||||||
|
.files .uploaded .dots-container { display: inline-block; width: 0px; padding-right: 65px;; }
|
||||||
|
.files .td.buttons { width: 40px; padding-left: 0px; padding-right: 0px; }
|
||||||
|
.files .td.buttons .edit {
|
||||||
|
background-color: #2196f336; border-radius: 15px; padding: 1px 9px; font-size: 80%; text-decoration: none; color: #1976D2;
|
||||||
|
}
|
||||||
|
.files .checkbox-outer { padding: 15px; padding-left: 20px; padding-right: 0px; }
|
||||||
|
.files .checkbox {
|
||||||
|
display: inline-block; width: 12px; height: 12px; border: 2px solid #00000014;
|
||||||
|
border-radius: 3px; vertical-align: -3px; margin-right: 10px;
|
||||||
|
}
|
||||||
|
.files .selected .checkbox { border-color: #dedede }
|
||||||
|
.files .selected .checkbox:after {
|
||||||
|
background-color: #dedede; content: ""; text-decoration: none; display: block; width: 10px; height: 10px; margin-left: 1px; margin-top: 1px;
|
||||||
|
}
|
||||||
|
.files .tbody .td.size { font-size: 13px }
|
||||||
|
.files .tbody .td.added, #PageFiles .files .td.access { font-size: 12px; color: #999 }
|
||||||
|
.files .tr.type-dir .name { font-weight: bold; }
|
||||||
|
.files .tr.type-parent .name .link { display: inline-block; width: 100%; padding: 5px; margin-top: -5px; }
|
||||||
|
.files .foot .td { color: #a4a4a4; background-color: #f7f7fc; }
|
||||||
|
.files .foot .create { float: right; text-decoration: none; position: relative; }
|
||||||
|
.files .foot .create .link { color: #8c42ed; text-decoration: none; }
|
||||||
|
.files .foot .create .link:active { background-color: #8c42ed3b; outline: 5px solid #8c42ed3b; }
|
||||||
|
.files .foot .create .menu { top: 40px; }
|
||||||
|
|
||||||
|
|
||||||
|
/* Editor */
|
||||||
|
|
||||||
|
.editor { background-color: #F7F7FC; float: left; width: calc(100% - 280px); box-sizing: border-box; transition: all 0.6s; }
|
||||||
|
.editor .CodeMirror { height: calc(100vh - 73px); visibility: hidden; }
|
||||||
|
.editor textarea { width: 100%; height: 800px; white-space: pre; }
|
||||||
|
.editor .title { margin-left: 20px; }
|
||||||
|
.editor .editor-head {
|
||||||
|
padding: 15px 20px; padding-left: 45px; font-size: 18px; font-weight: lighter; border: 1px solid #EEEEF5;
|
||||||
|
white-space: nowrap; overflow: hidden;
|
||||||
|
}
|
||||||
|
.editor.loaded .CodeMirror { visibility: inherit; }
|
||||||
|
.editor.error .CodeMirror { display: none; }
|
||||||
|
.editor .button.save { min-width: 30px; text-align: center; transition: all 0.3s; }
|
||||||
|
.editor .button.save.done { min-width: 80px; }
|
||||||
|
.editor .error-message { text-align: center; padding: 50px; }
|
||||||
|
|
||||||
|
.editor .CodeMirror-foldmarker {
|
||||||
|
line-height: .3; cursor: pointer; background-color: #ffeb3b61; text-shadow: none; font-family: inherit;
|
||||||
|
color: #050505; border: 1px solid #ffdf7f; padding: 0px 5px;
|
||||||
|
}
|
||||||
|
.editor .CodeMirror-activeline-background { background-color: #F6F6F6 !important; }
|
BIN
plugins/UiFileManager/media/img/loading.gif
Normal file
BIN
plugins/UiFileManager/media/img/loading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 723 B |
171
plugins/UiFileManager/media/js/FileEditor.coffee
Normal file
171
plugins/UiFileManager/media/js/FileEditor.coffee
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
class FileEditor extends Class
|
||||||
|
constructor: (@inner_path) ->
|
||||||
|
@need_update = true
|
||||||
|
@on_loaded = new Promise()
|
||||||
|
@is_loading = false
|
||||||
|
@content = ""
|
||||||
|
@node_cm = null
|
||||||
|
@cm = null
|
||||||
|
@error = null
|
||||||
|
@is_loaded = false
|
||||||
|
@is_modified = false
|
||||||
|
@is_saving = false
|
||||||
|
@mode = "Loading"
|
||||||
|
|
||||||
|
update: ->
|
||||||
|
is_required = Page.url_params.get("edit_mode") != "new"
|
||||||
|
|
||||||
|
Page.cmd "fileGet", {inner_path: @inner_path, required: is_required}, (res) =>
|
||||||
|
if res?.error
|
||||||
|
@error = res.error
|
||||||
|
@content = res.error
|
||||||
|
@log "Error loading: #{@error}"
|
||||||
|
else
|
||||||
|
if res
|
||||||
|
@content = res
|
||||||
|
else
|
||||||
|
@content = ""
|
||||||
|
@mode = "Create"
|
||||||
|
if not @content
|
||||||
|
@cm.getDoc().clearHistory()
|
||||||
|
@cm.setValue(@content)
|
||||||
|
if not @error
|
||||||
|
@is_loaded = true
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
|
||||||
|
isModified: =>
|
||||||
|
return @content != @cm.getValue()
|
||||||
|
|
||||||
|
storeCmNode: (node) =>
|
||||||
|
@node_cm = node
|
||||||
|
|
||||||
|
getMode: (inner_path) ->
|
||||||
|
ext = inner_path.split(".").pop()
|
||||||
|
types = {
|
||||||
|
"py": "python",
|
||||||
|
"json": "application/json",
|
||||||
|
"js": "javascript",
|
||||||
|
"coffee": "coffeescript",
|
||||||
|
"html": "htmlmixed",
|
||||||
|
"htm": "htmlmixed",
|
||||||
|
"php": "htmlmixed",
|
||||||
|
"rs": "rust",
|
||||||
|
"css": "css",
|
||||||
|
"md": "markdown",
|
||||||
|
"xml": "xml",
|
||||||
|
"svg": "xml"
|
||||||
|
}
|
||||||
|
return types[ext]
|
||||||
|
|
||||||
|
foldJson: (from, to) =>
|
||||||
|
@log "foldJson", from, to
|
||||||
|
# Get open / close token
|
||||||
|
startToken = '{'
|
||||||
|
endToken = '}'
|
||||||
|
prevLine = @cm.getLine(from.line)
|
||||||
|
if prevLine.lastIndexOf('[') > prevLine.lastIndexOf('{')
|
||||||
|
startToken = '['
|
||||||
|
endToken = ']'
|
||||||
|
|
||||||
|
# Get json content
|
||||||
|
internal = @cm.getRange(from, to)
|
||||||
|
toParse = startToken + internal + endToken
|
||||||
|
|
||||||
|
#Get key count
|
||||||
|
try
|
||||||
|
parsed = JSON.parse(toParse)
|
||||||
|
count = Object.keys(parsed).length
|
||||||
|
catch e
|
||||||
|
null
|
||||||
|
|
||||||
|
return if count then "\u21A4#{count}\u21A6" else "\u2194"
|
||||||
|
|
||||||
|
createCodeMirror: ->
|
||||||
|
mode = @getMode(@inner_path)
|
||||||
|
@log "Creating CodeMirror", @inner_path, mode
|
||||||
|
options = {
|
||||||
|
value: "Loading...",
|
||||||
|
mode: mode,
|
||||||
|
lineNumbers: true,
|
||||||
|
styleActiveLine: true,
|
||||||
|
matchBrackets: true,
|
||||||
|
keyMap: "sublime",
|
||||||
|
theme: "mdn-like",
|
||||||
|
extraKeys: {"Ctrl-Space": "autocomplete"},
|
||||||
|
foldGutter: true,
|
||||||
|
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]
|
||||||
|
|
||||||
|
}
|
||||||
|
if mode == "application/json"
|
||||||
|
options.gutters.unshift("CodeMirror-lint-markers")
|
||||||
|
options.lint = true
|
||||||
|
options.foldOptions = { widget: @foldJson }
|
||||||
|
|
||||||
|
@cm = CodeMirror(@node_cm, options)
|
||||||
|
@cm.on "changes", (changes) =>
|
||||||
|
if @is_loaded and not @is_modified
|
||||||
|
@is_modified = true
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
|
||||||
|
|
||||||
|
loadEditor: ->
|
||||||
|
if not @is_loading
|
||||||
|
document.getElementsByTagName("head")[0].insertAdjacentHTML(
|
||||||
|
"beforeend",
|
||||||
|
"""<link rel="stylesheet" href="codemirror/all.css" />"""
|
||||||
|
)
|
||||||
|
script = document.createElement('script')
|
||||||
|
script.src = "codemirror/all.js"
|
||||||
|
script.onload = =>
|
||||||
|
@createCodeMirror()
|
||||||
|
@on_loaded.resolve()
|
||||||
|
document.head.appendChild(script)
|
||||||
|
return @on_loaded
|
||||||
|
|
||||||
|
handleSidebarButtonClick: =>
|
||||||
|
Page.is_sidebar_closed = not Page.is_sidebar_closed
|
||||||
|
return false
|
||||||
|
|
||||||
|
handleSaveClick: =>
|
||||||
|
@is_saving = true
|
||||||
|
Page.cmd "fileWrite", [@inner_path, Text.fileEncode(@cm.getValue())], (res) =>
|
||||||
|
@is_saving = false
|
||||||
|
if res.error
|
||||||
|
Page.cmd "wrapperNotification", ["error", "Error saving #{res.error}"]
|
||||||
|
else
|
||||||
|
@is_save_done = true
|
||||||
|
setTimeout (() =>
|
||||||
|
@is_save_done = false
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
), 2000
|
||||||
|
@content = @cm.getValue()
|
||||||
|
@is_modified = false
|
||||||
|
if @mode == "Create"
|
||||||
|
@mode = "Edit"
|
||||||
|
Page.file_list.need_update = true
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
return false
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
if @need_update
|
||||||
|
@loadEditor().then =>
|
||||||
|
@update()
|
||||||
|
@need_update = false
|
||||||
|
h("div.editor", {afterCreate: @storeCmNode, classes: {error: @error, loaded: @is_loaded}}, [
|
||||||
|
h("a.sidebar-button", {href: "#Sidebar", onclick: @handleSidebarButtonClick}, h("span", "\u2039")),
|
||||||
|
h("div.editor-head", [
|
||||||
|
if @mode in ["Edit", "Create"]
|
||||||
|
h("a.save.button",
|
||||||
|
{href: "#Save", classes: {loading: @is_saving, done: @is_save_done, disabled: not @is_modified}, onclick: @handleSaveClick},
|
||||||
|
if @is_save_done then "Save: done!" else "Save"
|
||||||
|
)
|
||||||
|
h("span.title", @mode, ": ", @inner_path)
|
||||||
|
]),
|
||||||
|
if @error
|
||||||
|
h("div.error-message",
|
||||||
|
h("h2", "Unable to load the file: #{@error}")
|
||||||
|
h("a", {href: Page.file_list.getHref(@inner_path)}, "View in browser")
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
window.FileEditor = FileEditor
|
194
plugins/UiFileManager/media/js/FileItemList.coffee
Normal file
194
plugins/UiFileManager/media/js/FileItemList.coffee
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
class FileItemList extends Class
|
||||||
|
constructor: (@inner_path) ->
|
||||||
|
@items = []
|
||||||
|
@updating = false
|
||||||
|
@files_modified = {}
|
||||||
|
@dirs_modified = {}
|
||||||
|
@files_added = {}
|
||||||
|
@dirs_added = {}
|
||||||
|
@files_optional = {}
|
||||||
|
@items_by_name = {}
|
||||||
|
|
||||||
|
# Update item list
|
||||||
|
update: (cb) ->
|
||||||
|
@updating = true
|
||||||
|
@logStart("Updating dirlist")
|
||||||
|
Page.cmd "dirList", {inner_path: @inner_path, stats: true}, (res) =>
|
||||||
|
if res.error
|
||||||
|
@error = res.error
|
||||||
|
else
|
||||||
|
@error = null
|
||||||
|
pattern_ignore = RegExp("^" + Page.site_info.content?.ignore)
|
||||||
|
|
||||||
|
@items.splice(0, @items.length) # Remove all items
|
||||||
|
|
||||||
|
@items_by_name = {}
|
||||||
|
for row in res
|
||||||
|
row.type = @getFileType(row)
|
||||||
|
row.inner_path = @inner_path + row.name
|
||||||
|
if Page.site_info.content?.ignore and row.inner_path.match(pattern_ignore)
|
||||||
|
row.ignored = true
|
||||||
|
@items.push(row)
|
||||||
|
@items_by_name[row.name] = row
|
||||||
|
|
||||||
|
@sort()
|
||||||
|
|
||||||
|
if Page.site_info?.settings?.own
|
||||||
|
@updateAddedFiles()
|
||||||
|
|
||||||
|
@updateOptionalFiles =>
|
||||||
|
@updating = false
|
||||||
|
cb?()
|
||||||
|
@logEnd("Updating dirlist", @inner_path)
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
|
||||||
|
@updateModifiedFiles =>
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
|
||||||
|
|
||||||
|
updateModifiedFiles: (cb) =>
|
||||||
|
# Add modified attribute to changed files
|
||||||
|
Page.cmd "siteListModifiedFiles", [], (res) =>
|
||||||
|
@files_modified = {}
|
||||||
|
@dirs_modified = {}
|
||||||
|
for inner_path in res.modified_files
|
||||||
|
@files_modified[inner_path] = true
|
||||||
|
dir_inner_path = ""
|
||||||
|
dir_parts = inner_path.split("/")
|
||||||
|
for dir_part in dir_parts[..-2]
|
||||||
|
if dir_inner_path
|
||||||
|
dir_inner_path += "/#{dir_part}"
|
||||||
|
else
|
||||||
|
dir_inner_path = dir_part
|
||||||
|
@dirs_modified[dir_inner_path] = true
|
||||||
|
|
||||||
|
cb?()
|
||||||
|
|
||||||
|
# Update newly added items list since last sign
|
||||||
|
updateAddedFiles: =>
|
||||||
|
Page.cmd "fileGet", "content.json", (res) =>
|
||||||
|
if not res
|
||||||
|
return false
|
||||||
|
|
||||||
|
content = JSON.parse(res)
|
||||||
|
|
||||||
|
# Check new files
|
||||||
|
if not content.files?
|
||||||
|
return false
|
||||||
|
|
||||||
|
@files_added = {}
|
||||||
|
|
||||||
|
for file in @items
|
||||||
|
if file.name == "content.json" or file.is_dir
|
||||||
|
continue
|
||||||
|
if not content.files[@inner_path + file.name]
|
||||||
|
@files_added[@inner_path + file.name] = true
|
||||||
|
|
||||||
|
# Check new dirs
|
||||||
|
@dirs_added = {}
|
||||||
|
|
||||||
|
dirs_content = {}
|
||||||
|
for file_name of Object.assign({}, content.files, content.files_optional)
|
||||||
|
if not file_name.startsWith(@inner_path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
pattern = new RegExp("#{@inner_path}(.*?)/")
|
||||||
|
match = file_name.match(pattern)
|
||||||
|
|
||||||
|
if not match
|
||||||
|
continue
|
||||||
|
|
||||||
|
dirs_content[match[1]] = true
|
||||||
|
|
||||||
|
for file in @items
|
||||||
|
if not file.is_dir
|
||||||
|
continue
|
||||||
|
if not dirs_content[file.name]
|
||||||
|
@dirs_added[@inner_path + file.name] = true
|
||||||
|
|
||||||
|
# Update optional files list
|
||||||
|
updateOptionalFiles: (cb) =>
|
||||||
|
Page.cmd "optionalFileList", {filter: ""}, (res) =>
|
||||||
|
@files_optional = {}
|
||||||
|
for optional_file in res
|
||||||
|
@files_optional[optional_file.inner_path] = optional_file
|
||||||
|
|
||||||
|
@addOptionalFilesToItems()
|
||||||
|
|
||||||
|
cb?()
|
||||||
|
|
||||||
|
# Add optional files to item list
|
||||||
|
addOptionalFilesToItems: =>
|
||||||
|
is_added = false
|
||||||
|
for inner_path, optional_file of @files_optional
|
||||||
|
if optional_file.inner_path.startsWith(@inner_path)
|
||||||
|
if @getDirectory(optional_file.inner_path) == @inner_path
|
||||||
|
# Add optional file to list
|
||||||
|
file_name = @getFileName(optional_file.inner_path)
|
||||||
|
if not @items_by_name[file_name]
|
||||||
|
row = {
|
||||||
|
"name": file_name, "type": "file", "optional_empty": true,
|
||||||
|
"size": optional_file.size, "is_dir": false, "inner_path": optional_file.inner_path
|
||||||
|
}
|
||||||
|
@items.push(row)
|
||||||
|
@items_by_name[file_name] = row
|
||||||
|
is_added = true
|
||||||
|
else
|
||||||
|
# Add optional dir to list
|
||||||
|
dir_name = optional_file.inner_path.replace(@inner_path, "").match(/(.*?)\//, "")?[1]
|
||||||
|
if dir_name and not @items_by_name[dir_name]
|
||||||
|
row = {
|
||||||
|
"name": dir_name, "type": "dir", "optional_empty": true,
|
||||||
|
"size": 0, "is_dir": true, "inner_path": optional_file.inner_path
|
||||||
|
}
|
||||||
|
@items.push(row)
|
||||||
|
@items_by_name[dir_name] = row
|
||||||
|
is_added = true
|
||||||
|
|
||||||
|
if is_added
|
||||||
|
@sort()
|
||||||
|
|
||||||
|
getFileType: (file) =>
|
||||||
|
if file.is_dir
|
||||||
|
return "dir"
|
||||||
|
else
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
getDirectory: (inner_path) ->
|
||||||
|
if inner_path.indexOf("/") != -1
|
||||||
|
return inner_path.replace(/^(.*\/)(.*?)$/, "$1")
|
||||||
|
else
|
||||||
|
return ""
|
||||||
|
|
||||||
|
getFileName: (inner_path) ->
|
||||||
|
return inner_path.replace(/^(.*\/)(.*?)$/, "$2")
|
||||||
|
|
||||||
|
|
||||||
|
isModified: (inner_path) =>
|
||||||
|
return @files_modified[inner_path] or @dirs_modified[inner_path]
|
||||||
|
|
||||||
|
isAdded: (inner_path) =>
|
||||||
|
return @files_added[inner_path] or @dirs_added[inner_path]
|
||||||
|
|
||||||
|
hasPermissionDelete: (file) =>
|
||||||
|
if file.type in ["dir", "parent"]
|
||||||
|
return false
|
||||||
|
|
||||||
|
if file.inner_path == "content.json"
|
||||||
|
return false
|
||||||
|
|
||||||
|
optional_info = @getOptionalInfo(file.inner_path)
|
||||||
|
if optional_info and optional_info.downloaded_percent > 0
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return Page.site_info?.settings?.own
|
||||||
|
|
||||||
|
getOptionalInfo: (inner_path) =>
|
||||||
|
return @files_optional[inner_path]
|
||||||
|
|
||||||
|
sort: =>
|
||||||
|
@items.sort (a, b) ->
|
||||||
|
return (b.is_dir - a.is_dir) || a.name.localeCompare(b.name)
|
||||||
|
|
||||||
|
|
||||||
|
window.FileItemList = FileItemList
|
270
plugins/UiFileManager/media/js/FileList.coffee
Normal file
270
plugins/UiFileManager/media/js/FileList.coffee
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
BINARY_EXTENSIONS = ["png", "gif", "jpg", "pdf", "doc", "msgpack", "zip", "rar", "gz", "tar", "exe"]
|
||||||
|
|
||||||
|
class FileList extends Class
|
||||||
|
constructor: (@site, @inner_path, @is_owner=false) ->
|
||||||
|
@need_update = true
|
||||||
|
@error = null
|
||||||
|
@url_root = "/list/" + @site + "/"
|
||||||
|
if @inner_path
|
||||||
|
@inner_path += "/"
|
||||||
|
@url_root += @inner_path
|
||||||
|
@log("inited", @url_root)
|
||||||
|
@item_list = new FileItemList(@inner_path)
|
||||||
|
@item_list.items = @item_list.items
|
||||||
|
@menu_create = new Menu()
|
||||||
|
|
||||||
|
@select_action = null
|
||||||
|
@selected = {}
|
||||||
|
@selected_items_num = 0
|
||||||
|
@selected_items_size = 0
|
||||||
|
@selected_optional_empty_num = 0
|
||||||
|
|
||||||
|
isSelectedAll: ->
|
||||||
|
false
|
||||||
|
|
||||||
|
update: =>
|
||||||
|
@item_list.update =>
|
||||||
|
document.body.classList.add("loaded")
|
||||||
|
|
||||||
|
getHref: (inner_path) =>
|
||||||
|
return "/" + @site + "/" + inner_path
|
||||||
|
|
||||||
|
getListHref: (inner_path) =>
|
||||||
|
return "/list/" + @site + "/" + inner_path
|
||||||
|
|
||||||
|
getEditHref: (inner_path, mode=null) =>
|
||||||
|
href = @url_root + "?file=" + inner_path
|
||||||
|
if mode
|
||||||
|
href += "&edit_mode=#{mode}"
|
||||||
|
return href
|
||||||
|
|
||||||
|
checkSelectedItems: =>
|
||||||
|
@selected_items_num = 0
|
||||||
|
@selected_items_size = 0
|
||||||
|
@selected_optional_empty_num = 0
|
||||||
|
for item in @item_list.items
|
||||||
|
if @selected[item.inner_path]
|
||||||
|
@selected_items_num += 1
|
||||||
|
@selected_items_size += item.size
|
||||||
|
optional_info = @item_list.getOptionalInfo(item.inner_path)
|
||||||
|
if optional_info and not optional_info.downloaded_percent > 0
|
||||||
|
@selected_optional_empty_num += 1
|
||||||
|
|
||||||
|
handleMenuCreateClick: =>
|
||||||
|
@menu_create.items = []
|
||||||
|
@menu_create.items.push ["File", @handleNewFileClick]
|
||||||
|
@menu_create.items.push ["Directory", @handleNewDirectoryClick]
|
||||||
|
@menu_create.toggle()
|
||||||
|
return false
|
||||||
|
|
||||||
|
handleNewFileClick: =>
|
||||||
|
Page.cmd "wrapperPrompt", "New file name:", (file_name) =>
|
||||||
|
window.top.location.href = @getEditHref(@inner_path + file_name, "new")
|
||||||
|
return false
|
||||||
|
|
||||||
|
handleNewDirectoryClick: =>
|
||||||
|
Page.cmd "wrapperPrompt", "New directory name:", (res) =>
|
||||||
|
alert("directory name #{res}")
|
||||||
|
return false
|
||||||
|
|
||||||
|
handleSelectClick: (e) =>
|
||||||
|
return false
|
||||||
|
|
||||||
|
handleSelectEnd: (e) =>
|
||||||
|
document.body.removeEventListener('mouseup', @handleSelectEnd)
|
||||||
|
@select_action = null
|
||||||
|
|
||||||
|
handleSelectMousedown: (e) =>
|
||||||
|
inner_path = e.currentTarget.attributes.inner_path.value
|
||||||
|
if @selected[inner_path]
|
||||||
|
delete @selected[inner_path]
|
||||||
|
@select_action = "deselect"
|
||||||
|
else
|
||||||
|
@selected[inner_path] = true
|
||||||
|
@select_action = "select"
|
||||||
|
@checkSelectedItems()
|
||||||
|
document.body.addEventListener('mouseup', @handleSelectEnd)
|
||||||
|
e.stopPropagation()
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
return false
|
||||||
|
|
||||||
|
handleRowMouseenter: (e) =>
|
||||||
|
if e.buttons and @select_action
|
||||||
|
inner_path = e.target.attributes.inner_path.value
|
||||||
|
if @select_action == "select"
|
||||||
|
@selected[inner_path] = true
|
||||||
|
else
|
||||||
|
delete @selected[inner_path]
|
||||||
|
@checkSelectedItems()
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
return false
|
||||||
|
|
||||||
|
handleSelectbarCancel: =>
|
||||||
|
@selected = {}
|
||||||
|
@checkSelectedItems()
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
return false
|
||||||
|
|
||||||
|
handleSelectbarDelete: (e, remove_optional=false) =>
|
||||||
|
for inner_path of @selected
|
||||||
|
optional_info = @item_list.getOptionalInfo(inner_path)
|
||||||
|
delete @selected[inner_path]
|
||||||
|
if optional_info and not remove_optional
|
||||||
|
Page.cmd "optionalFileDelete", inner_path
|
||||||
|
else
|
||||||
|
Page.cmd "fileDelete", inner_path
|
||||||
|
@need_update = true
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
@checkSelectedItems()
|
||||||
|
return false
|
||||||
|
|
||||||
|
handleSelectbarRemoveOptional: (e) =>
|
||||||
|
return @handleSelectbarDelete(e, true)
|
||||||
|
|
||||||
|
renderSelectbar: =>
|
||||||
|
h("div.selectbar", {classes: {visible: @selected_items_num > 0}}, [
|
||||||
|
"Selected:",
|
||||||
|
h("span.info", [
|
||||||
|
h("span.num", "#{@selected_items_num} files"),
|
||||||
|
h("span.size", "(#{Text.formatSize(@selected_items_size)})"),
|
||||||
|
])
|
||||||
|
h("div.actions", [
|
||||||
|
if @selected_optional_empty_num > 0
|
||||||
|
h("a.action.delete.remove_optional", {href: "#", onclick: @handleSelectbarRemoveOptional}, "Delete and remove optional")
|
||||||
|
else
|
||||||
|
h("a.action.delete", {href: "#", onclick: @handleSelectbarDelete}, "Delete")
|
||||||
|
])
|
||||||
|
h("a.cancel.link", {href: "#", onclick: @handleSelectbarCancel}, "Cancel")
|
||||||
|
])
|
||||||
|
|
||||||
|
renderHead: =>
|
||||||
|
parent_links = []
|
||||||
|
inner_path_parent = ""
|
||||||
|
for parent_dir in @inner_path.split("/")
|
||||||
|
if not parent_dir
|
||||||
|
continue
|
||||||
|
if inner_path_parent
|
||||||
|
inner_path_parent += "/"
|
||||||
|
inner_path_parent += "#{parent_dir}"
|
||||||
|
parent_links.push(
|
||||||
|
[" / ", h("a", {href: @getListHref(inner_path_parent)}, parent_dir)]
|
||||||
|
)
|
||||||
|
return h("div.tr.thead", h("div.td.full",
|
||||||
|
h("a", {href: @getListHref("")}, "root"),
|
||||||
|
parent_links
|
||||||
|
))
|
||||||
|
|
||||||
|
renderItemCheckbox: (item) =>
|
||||||
|
if not @item_list.hasPermissionDelete(item)
|
||||||
|
return [" "]
|
||||||
|
|
||||||
|
return h("a.checkbox-outer", {
|
||||||
|
href: "#Select",
|
||||||
|
onmousedown: @handleSelectMousedown,
|
||||||
|
onclick: @handleSelectClick,
|
||||||
|
inner_path: item.inner_path
|
||||||
|
}, h("span.checkbox"))
|
||||||
|
|
||||||
|
renderItem: (item) =>
|
||||||
|
if item.type == "parent"
|
||||||
|
href = @url_root.replace(/^(.*)\/.{2,255}?$/, "$1/")
|
||||||
|
else if item.type == "dir"
|
||||||
|
href = @url_root + item.name
|
||||||
|
else
|
||||||
|
href = @url_root.replace(/^\/list\//, "/") + item.name
|
||||||
|
|
||||||
|
inner_path = @inner_path + item.name
|
||||||
|
href_edit = @getEditHref(inner_path)
|
||||||
|
is_dir = item.type in ["dir", "parent"]
|
||||||
|
ext = item.name.split(".").pop()
|
||||||
|
|
||||||
|
is_editing = inner_path == Page.file_editor?.inner_path
|
||||||
|
is_editable = not is_dir and item.size < 1024 * 1024 and ext not in BINARY_EXTENSIONS
|
||||||
|
is_modified = @item_list.isModified(inner_path)
|
||||||
|
is_added = @item_list.isAdded(inner_path)
|
||||||
|
optional_info = @item_list.getOptionalInfo(inner_path)
|
||||||
|
|
||||||
|
style = ""
|
||||||
|
title = ""
|
||||||
|
|
||||||
|
if optional_info
|
||||||
|
downloaded_percent = optional_info.downloaded_percent
|
||||||
|
if not downloaded_percent
|
||||||
|
downloaded_percent = 0
|
||||||
|
style += "background: linear-gradient(90deg, #fff6dd, #{downloaded_percent}%, white, #{downloaded_percent}%, white);"
|
||||||
|
is_added = false
|
||||||
|
|
||||||
|
if item.ignored
|
||||||
|
is_added = false
|
||||||
|
|
||||||
|
if is_modified then title += " (modified)"
|
||||||
|
if is_added then title += " (new)"
|
||||||
|
if optional_info or item.optional_empty then title += " (optional)"
|
||||||
|
if item.ignored then title += " (ignored from content.json)"
|
||||||
|
|
||||||
|
classes = {
|
||||||
|
"type-#{item.type}": true, editing: is_editing, nobuttons: not is_editable, selected: @selected[inner_path],
|
||||||
|
modified: is_modified, added: is_added, ignored: item.ignored, optional: optional_info, optional_empty: item.optional_empty
|
||||||
|
}
|
||||||
|
|
||||||
|
h("div.tr", {key: item.name, classes: classes, style: style, onmouseenter: @handleRowMouseenter, inner_path: inner_path}, [
|
||||||
|
h("div.td.pre", {title: title},
|
||||||
|
@renderItemCheckbox(item)
|
||||||
|
),
|
||||||
|
h("div.td.name", h("a.link", {href: href}, item.name))
|
||||||
|
h("div.td.buttons", if is_editable then h("a.edit", {href: href_edit}, if Page.site_info.settings.own then "Edit" else "View"))
|
||||||
|
h("div.td.size", if is_dir then "[DIR]" else Text.formatSize(item.size))
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
renderItems: =>
|
||||||
|
return [
|
||||||
|
if @item_list.error and not @item_list.items.length and not @item_list.updating then [
|
||||||
|
h("div.tr", {key: "error"}, h("div.td.full.error", @item_list.error))
|
||||||
|
],
|
||||||
|
if @inner_path then @renderItem({"name": "..", type: "parent", size: 0})
|
||||||
|
@item_list.items.map @renderItem
|
||||||
|
]
|
||||||
|
|
||||||
|
renderFoot: =>
|
||||||
|
files = (item for item in @item_list.items when item.type not in ["parent", "dir"])
|
||||||
|
dirs = (item for item in @item_list.items when item.type == "dir")
|
||||||
|
if files.length
|
||||||
|
total_size = (item.size for file in files).reduce (a, b) -> a + b
|
||||||
|
else
|
||||||
|
total_size = 0
|
||||||
|
|
||||||
|
foot_text = "Total: "
|
||||||
|
foot_text += "#{dirs.length} dir, #{files.length} file in #{Text.formatSize(total_size)}"
|
||||||
|
|
||||||
|
return [
|
||||||
|
if dirs.length or files.length or Page.site_info?.settings?.own
|
||||||
|
h("div.tr.foot-info.foot", h("div.td.full", [
|
||||||
|
if @item_list.updating
|
||||||
|
"Updating file list..."
|
||||||
|
else
|
||||||
|
if dirs.length or files.length then foot_text
|
||||||
|
if Page.site_info?.settings?.own
|
||||||
|
h("div.create", [
|
||||||
|
h("a.link", {href: "#Create+new+file", onclick: @handleNewFileClick}, "+ New")
|
||||||
|
@menu_create.render()
|
||||||
|
])
|
||||||
|
]))
|
||||||
|
]
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
if @need_update
|
||||||
|
@update()
|
||||||
|
@need_update = false
|
||||||
|
|
||||||
|
if not @item_list.items
|
||||||
|
return []
|
||||||
|
|
||||||
|
return h("div.files", [
|
||||||
|
@renderSelectbar(),
|
||||||
|
@renderHead(),
|
||||||
|
h("div.tbody", @renderItems()),
|
||||||
|
@renderFoot()
|
||||||
|
])
|
||||||
|
|
||||||
|
window.FileList = FileList
|
79
plugins/UiFileManager/media/js/UiFileManager.coffee
Normal file
79
plugins/UiFileManager/media/js/UiFileManager.coffee
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
window.h = maquette.h
|
||||||
|
|
||||||
|
class UiFileManager extends ZeroFrame
|
||||||
|
init: ->
|
||||||
|
@url_params = new URLSearchParams(window.location.search)
|
||||||
|
@list_site = @url_params.get("site")
|
||||||
|
@list_address = @url_params.get("address")
|
||||||
|
@list_inner_path = @url_params.get("inner_path")
|
||||||
|
@editor_inner_path = @url_params.get("file")
|
||||||
|
@file_list = new FileList(@list_site, @list_inner_path)
|
||||||
|
|
||||||
|
@site_info = null
|
||||||
|
@server_info = null
|
||||||
|
|
||||||
|
@is_sidebar_closed = false
|
||||||
|
|
||||||
|
if @editor_inner_path
|
||||||
|
@file_editor = new FileEditor(@editor_inner_path)
|
||||||
|
|
||||||
|
window.onbeforeunload = =>
|
||||||
|
if @file_editor?.isModified()
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
return null
|
||||||
|
|
||||||
|
window.onresize = =>
|
||||||
|
@checkBodyWidth()
|
||||||
|
|
||||||
|
@checkBodyWidth()
|
||||||
|
|
||||||
|
@cmd("wrapperSetViewport", "width=device-width, initial-scale=0.8")
|
||||||
|
|
||||||
|
@cmd "serverInfo", {}, (server_info) =>
|
||||||
|
@server_info = server_info
|
||||||
|
@cmd "siteInfo", {}, (site_info) =>
|
||||||
|
@cmd("wrapperSetTitle", "List: /#{@list_inner_path} - #{site_info.content.title} - ZeroNet")
|
||||||
|
@site_info = site_info
|
||||||
|
if @file_editor then @file_editor.on_loaded.then =>
|
||||||
|
@file_editor.cm.setOption("readOnly", not site_info.settings.own)
|
||||||
|
@file_editor.mode = if site_info.settings.own then "Edit" else "View"
|
||||||
|
@projector.scheduleRender()
|
||||||
|
|
||||||
|
checkBodyWidth: =>
|
||||||
|
if not @file_editor
|
||||||
|
return false
|
||||||
|
|
||||||
|
if document.body.offsetWidth < 960 and not @is_sidebar_closed
|
||||||
|
@is_sidebar_closed = true
|
||||||
|
@projector?.scheduleRender()
|
||||||
|
else if document.body.offsetWidth > 960 and @is_sidebar_closed
|
||||||
|
@is_sidebar_closed = false
|
||||||
|
@projector?.scheduleRender()
|
||||||
|
|
||||||
|
onRequest: (cmd, message) =>
|
||||||
|
if cmd == "setSiteInfo"
|
||||||
|
@site_info = message
|
||||||
|
RateLimitCb 1000, (cb_done) =>
|
||||||
|
@file_list.update(cb_done)
|
||||||
|
@projector.scheduleRender()
|
||||||
|
else if cmd == "setServerInfo"
|
||||||
|
@server_info = message
|
||||||
|
@projector.scheduleRender()
|
||||||
|
else
|
||||||
|
@log "Unknown incoming message:", cmd
|
||||||
|
|
||||||
|
createProjector: =>
|
||||||
|
@projector = maquette.createProjector()
|
||||||
|
@projector.replace($("#content"), @render)
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
return h("div.content#content", [
|
||||||
|
h("div.manager", {classes: {editing: @file_editor, sidebar_closed: @is_sidebar_closed}}, [
|
||||||
|
@file_list.render(),
|
||||||
|
if @file_editor then @file_editor.render()
|
||||||
|
])
|
||||||
|
])
|
||||||
|
|
||||||
|
window.Page = new UiFileManager()
|
||||||
|
window.Page.createProjector()
|
138
plugins/UiFileManager/media/js/lib/Animation.coffee
Normal file
138
plugins/UiFileManager/media/js/lib/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()
|
23
plugins/UiFileManager/media/js/lib/Class.coffee
Normal file
23
plugins/UiFileManager/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
|
3
plugins/UiFileManager/media/js/lib/Dollar.coffee
Normal file
3
plugins/UiFileManager/media/js/lib/Dollar.coffee
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
window.$ = (selector) ->
|
||||||
|
if selector.startsWith("#")
|
||||||
|
return document.getElementById(selector.replace("#", ""))
|
26
plugins/UiFileManager/media/js/lib/ItemList.coffee
Normal file
26
plugins/UiFileManager/media/js/lib/ItemList.coffee
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
class ItemList
|
||||||
|
constructor: (@item_class, @key) ->
|
||||||
|
@items = []
|
||||||
|
@items_bykey = {}
|
||||||
|
|
||||||
|
sync: (rows, item_class, key) ->
|
||||||
|
@items.splice(0, @items.length) # Empty items
|
||||||
|
for row in rows
|
||||||
|
current_obj = @items_bykey[row[@key]]
|
||||||
|
if current_obj
|
||||||
|
current_obj.row = row
|
||||||
|
@items.push current_obj
|
||||||
|
else
|
||||||
|
item = new @item_class(row, @)
|
||||||
|
@items_bykey[row[@key]] = item
|
||||||
|
@items.push item
|
||||||
|
|
||||||
|
deleteItem: (item) ->
|
||||||
|
index = @items.indexOf(item)
|
||||||
|
if index > -1
|
||||||
|
@items.splice(index, 1)
|
||||||
|
else
|
||||||
|
console.log "Can't delete item", item
|
||||||
|
delete @items_bykey[item.row[@key]]
|
||||||
|
|
||||||
|
window.ItemList = ItemList
|
110
plugins/UiFileManager/media/js/lib/Menu.coffee
Normal file
110
plugins/UiFileManager/media/js/lib/Menu.coffee
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
class Menu
|
||||||
|
constructor: ->
|
||||||
|
@visible = false
|
||||||
|
@items = []
|
||||||
|
@node = null
|
||||||
|
@height = 0
|
||||||
|
@direction = "bottom"
|
||||||
|
|
||||||
|
show: =>
|
||||||
|
window.visible_menu?.hide()
|
||||||
|
@visible = true
|
||||||
|
window.visible_menu = @
|
||||||
|
@direction = @getDirection()
|
||||||
|
|
||||||
|
hide: =>
|
||||||
|
@visible = false
|
||||||
|
|
||||||
|
toggle: =>
|
||||||
|
if @visible
|
||||||
|
@hide()
|
||||||
|
else
|
||||||
|
@show()
|
||||||
|
Page.projector.scheduleRender()
|
||||||
|
|
||||||
|
|
||||||
|
addItem: (title, cb, selected=false) ->
|
||||||
|
@items.push([title, cb, selected])
|
||||||
|
|
||||||
|
|
||||||
|
storeNode: (node) =>
|
||||||
|
@node = node
|
||||||
|
# Animate visible
|
||||||
|
if @visible
|
||||||
|
node.className = node.className.replace("visible", "")
|
||||||
|
setTimeout (=>
|
||||||
|
node.className += " visible"
|
||||||
|
node.attributes.style.value = @getStyle()
|
||||||
|
), 20
|
||||||
|
node.style.maxHeight = "none"
|
||||||
|
@height = node.offsetHeight
|
||||||
|
node.style.maxHeight = "0px"
|
||||||
|
@direction = @getDirection()
|
||||||
|
|
||||||
|
getDirection: =>
|
||||||
|
if @node and @node.parentNode.getBoundingClientRect().top + @height + 60 > document.body.clientHeight and @node.parentNode.getBoundingClientRect().top - @height > 0
|
||||||
|
return "top"
|
||||||
|
else
|
||||||
|
return "bottom"
|
||||||
|
|
||||||
|
handleClick: (e) =>
|
||||||
|
keep_menu = false
|
||||||
|
for item in @items
|
||||||
|
[title, cb, selected] = item
|
||||||
|
if title == e.currentTarget.textContent or e.currentTarget["data-title"] == title
|
||||||
|
keep_menu = cb?(item)
|
||||||
|
break
|
||||||
|
if keep_menu != true and cb != null
|
||||||
|
@hide()
|
||||||
|
return false
|
||||||
|
|
||||||
|
renderItem: (item) =>
|
||||||
|
[title, cb, selected] = item
|
||||||
|
if typeof(selected) == "function"
|
||||||
|
selected = selected()
|
||||||
|
|
||||||
|
if title == "---"
|
||||||
|
return h("div.menu-item-separator", {key: Time.timestamp()})
|
||||||
|
else
|
||||||
|
if cb == null
|
||||||
|
href = undefined
|
||||||
|
onclick = @handleClick
|
||||||
|
else if typeof(cb) == "string" # Url
|
||||||
|
href = cb
|
||||||
|
onclick = true
|
||||||
|
else # Callback
|
||||||
|
href = "#"+title
|
||||||
|
onclick = @handleClick
|
||||||
|
classes = {
|
||||||
|
"selected": selected,
|
||||||
|
"noaction": (cb == null)
|
||||||
|
}
|
||||||
|
return h("a.menu-item", {href: href, onclick: onclick, "data-title": title, key: title, classes: classes}, title)
|
||||||
|
|
||||||
|
getStyle: =>
|
||||||
|
if @visible
|
||||||
|
max_height = @height
|
||||||
|
else
|
||||||
|
max_height = 0
|
||||||
|
style = "max-height: #{max_height}px"
|
||||||
|
if @direction == "top"
|
||||||
|
style += ";margin-top: #{0 - @height - 50}px"
|
||||||
|
else
|
||||||
|
style += ";margin-top: 0px"
|
||||||
|
return style
|
||||||
|
|
||||||
|
render: (class_name="") =>
|
||||||
|
if @visible or @node
|
||||||
|
h("div.menu#{class_name}", {classes: {"visible": @visible}, style: @getStyle(), afterCreate: @storeNode}, @items.map(@renderItem))
|
||||||
|
|
||||||
|
window.Menu = Menu
|
||||||
|
|
||||||
|
# Hide menu on outside click
|
||||||
|
document.body.addEventListener "mouseup", (e) ->
|
||||||
|
if not window.visible_menu or not window.visible_menu.node
|
||||||
|
return false
|
||||||
|
menu_node = window.visible_menu.node
|
||||||
|
menu_parents = [menu_node, menu_node.parentNode]
|
||||||
|
if e.target.parentNode not in menu_parents and e.target.parentNode.parentNode not in menu_parents
|
||||||
|
window.visible_menu.hide()
|
||||||
|
Page.projector.scheduleRender()
|
74
plugins/UiFileManager/media/js/lib/Promise.coffee
Normal file
74
plugins/UiFileManager/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
|
||||||
|
###
|
9
plugins/UiFileManager/media/js/lib/Prototypes.coffee
Normal file
9
plugins/UiFileManager/media/js/lib/Prototypes.coffee
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
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
|
||||||
|
|
62
plugins/UiFileManager/media/js/lib/RateLimitCb.coffee
Normal file
62
plugins/UiFileManager/media/js/lib/RateLimitCb.coffee
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
last_time = {}
|
||||||
|
calling = {}
|
||||||
|
calling_iterval = {}
|
||||||
|
call_after_interval = {}
|
||||||
|
|
||||||
|
# Rate limit function call and don't allow to run in parallel (until callback is called)
|
||||||
|
window.RateLimitCb = (interval, fn, args=[]) ->
|
||||||
|
cb = -> # Callback when function finished
|
||||||
|
left = interval - (Date.now() - last_time[fn]) # Time life until next call
|
||||||
|
# console.log "CB, left", left, "Calling:", calling[fn]
|
||||||
|
if left <= 0 # No time left from rate limit interval
|
||||||
|
delete last_time[fn]
|
||||||
|
if calling[fn] # Function called within interval
|
||||||
|
RateLimitCb(interval, fn, calling[fn])
|
||||||
|
delete calling[fn]
|
||||||
|
else # Time left from rate limit interval
|
||||||
|
setTimeout (->
|
||||||
|
delete last_time[fn]
|
||||||
|
if calling[fn] # Function called within interval
|
||||||
|
RateLimitCb(interval, fn, calling[fn])
|
||||||
|
delete calling[fn]
|
||||||
|
), left
|
||||||
|
if last_time[fn] # Function called within interval
|
||||||
|
calling[fn] = args # Schedule call and update arguments
|
||||||
|
else # Not called within interval, call instantly
|
||||||
|
last_time[fn] = Date.now()
|
||||||
|
fn.apply(this, [cb, args...])
|
||||||
|
|
||||||
|
|
||||||
|
window.RateLimit = (interval, fn) ->
|
||||||
|
if calling_iterval[fn] > interval
|
||||||
|
clearInterval calling[fn]
|
||||||
|
delete calling[fn]
|
||||||
|
|
||||||
|
if not calling[fn]
|
||||||
|
call_after_interval[fn] = false
|
||||||
|
fn() # First call is not delayed
|
||||||
|
calling_iterval[fn] = interval
|
||||||
|
calling[fn] = setTimeout (->
|
||||||
|
if call_after_interval[fn]
|
||||||
|
fn()
|
||||||
|
delete calling[fn]
|
||||||
|
delete call_after_interval[fn]
|
||||||
|
), interval
|
||||||
|
else # Called within iterval, delay the call
|
||||||
|
call_after_interval[fn] = true
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
window.s = Date.now()
|
||||||
|
window.load = (done, num) ->
|
||||||
|
console.log "Loading #{num}...", Date.now()-window.s
|
||||||
|
setTimeout (-> done()), 1000
|
||||||
|
|
||||||
|
RateLimit 500, window.load, [0] # Called instantly
|
||||||
|
RateLimit 500, window.load, [1]
|
||||||
|
setTimeout (-> RateLimit 500, window.load, [300]), 300
|
||||||
|
setTimeout (-> RateLimit 500, window.load, [600]), 600 # Called after 1000ms
|
||||||
|
setTimeout (-> RateLimit 500, window.load, [1000]), 1000
|
||||||
|
setTimeout (-> RateLimit 500, window.load, [1200]), 1200 # Called after 2000ms
|
||||||
|
setTimeout (-> RateLimit 500, window.load, [3000]), 3000 # Called after 3000ms
|
||||||
|
###
|
147
plugins/UiFileManager/media/js/lib/Text.coffee
Normal file
147
plugins/UiFileManager/media/js/lib/Text.coffee
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
class Text
|
||||||
|
toColor: (text, saturation=30, lightness=50) ->
|
||||||
|
hash = 0
|
||||||
|
for i in [0..text.length-1]
|
||||||
|
hash += text.charCodeAt(i)*i
|
||||||
|
hash = hash % 1777
|
||||||
|
return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)";
|
||||||
|
|
||||||
|
|
||||||
|
renderMarked: (text, options={}) ->
|
||||||
|
options["gfm"] = true
|
||||||
|
options["breaks"] = true
|
||||||
|
options["sanitize"] = true
|
||||||
|
options["renderer"] = marked_renderer
|
||||||
|
text = marked(text, options)
|
||||||
|
return @fixHtmlLinks text
|
||||||
|
|
||||||
|
emailLinks: (text) ->
|
||||||
|
return text.replace(/([a-zA-Z0-9]+)@zeroid.bit/g, "<a href='?to=$1' onclick='return Page.message_create.show(\"$1\")'>$1@zeroid.bit</a>")
|
||||||
|
|
||||||
|
# Convert zeronet html links to relaitve
|
||||||
|
fixHtmlLinks: (text) ->
|
||||||
|
if window.is_proxy
|
||||||
|
return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="http://zero')
|
||||||
|
else
|
||||||
|
return text.replace(/href="http:\/\/(127.0.0.1|localhost):43110/g, 'href="')
|
||||||
|
|
||||||
|
# Convert a single link to relative
|
||||||
|
fixLink: (link) ->
|
||||||
|
if window.is_proxy
|
||||||
|
back = link.replace(/http:\/\/(127.0.0.1|localhost):43110/, 'http://zero')
|
||||||
|
return back.replace(/http:\/\/zero\/([^\/]+\.bit)/, "http://$1") # Domain links
|
||||||
|
else
|
||||||
|
return link.replace(/http:\/\/(127.0.0.1|localhost):43110/, '')
|
||||||
|
|
||||||
|
toUrl: (text) ->
|
||||||
|
return text.replace(/[^A-Za-z0-9]/g, "+").replace(/[+]+/g, "+").replace(/[+]+$/, "")
|
||||||
|
|
||||||
|
getSiteUrl: (address) ->
|
||||||
|
if window.is_proxy
|
||||||
|
if "." in address # Domain
|
||||||
|
return "http://"+address+"/"
|
||||||
|
else
|
||||||
|
return "http://zero/"+address+"/"
|
||||||
|
else
|
||||||
|
return "/"+address+"/"
|
||||||
|
|
||||||
|
|
||||||
|
fixReply: (text) ->
|
||||||
|
return text.replace(/(>.*\n)([^\n>])/gm, "$1\n$2")
|
||||||
|
|
||||||
|
toBitcoinAddress: (text) ->
|
||||||
|
return text.replace(/[^A-Za-z0-9]/g, "")
|
||||||
|
|
||||||
|
|
||||||
|
jsonEncode: (obj) ->
|
||||||
|
return unescape(encodeURIComponent(JSON.stringify(obj)))
|
||||||
|
|
||||||
|
jsonDecode: (obj) ->
|
||||||
|
return JSON.parse(decodeURIComponent(escape(obj)))
|
||||||
|
|
||||||
|
fileEncode: (obj) ->
|
||||||
|
if typeof(obj) == "string"
|
||||||
|
return btoa(unescape(encodeURIComponent(obj)))
|
||||||
|
else
|
||||||
|
return btoa(unescape(encodeURIComponent(JSON.stringify(obj, undefined, '\t'))))
|
||||||
|
|
||||||
|
utf8Encode: (s) ->
|
||||||
|
return unescape(encodeURIComponent(s))
|
||||||
|
|
||||||
|
utf8Decode: (s) ->
|
||||||
|
return decodeURIComponent(escape(s))
|
||||||
|
|
||||||
|
|
||||||
|
distance: (s1, s2) ->
|
||||||
|
s1 = s1.toLocaleLowerCase()
|
||||||
|
s2 = s2.toLocaleLowerCase()
|
||||||
|
next_find_i = 0
|
||||||
|
next_find = s2[0]
|
||||||
|
match = true
|
||||||
|
extra_parts = {}
|
||||||
|
for char in s1
|
||||||
|
if char != next_find
|
||||||
|
if extra_parts[next_find_i]
|
||||||
|
extra_parts[next_find_i] += char
|
||||||
|
else
|
||||||
|
extra_parts[next_find_i] = char
|
||||||
|
else
|
||||||
|
next_find_i++
|
||||||
|
next_find = s2[next_find_i]
|
||||||
|
|
||||||
|
if extra_parts[next_find_i]
|
||||||
|
extra_parts[next_find_i] = "" # Extra chars on the end doesnt matter
|
||||||
|
extra_parts = (val for key, val of extra_parts)
|
||||||
|
if next_find_i >= s2.length
|
||||||
|
return extra_parts.length + extra_parts.join("").length
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
parseQuery: (query) ->
|
||||||
|
params = {}
|
||||||
|
parts = query.split('&')
|
||||||
|
for part in parts
|
||||||
|
[key, val] = part.split("=")
|
||||||
|
if val
|
||||||
|
params[decodeURIComponent(key)] = decodeURIComponent(val)
|
||||||
|
else
|
||||||
|
params["url"] = decodeURIComponent(key)
|
||||||
|
return params
|
||||||
|
|
||||||
|
encodeQuery: (params) ->
|
||||||
|
back = []
|
||||||
|
if params.url
|
||||||
|
back.push(params.url)
|
||||||
|
for key, val of params
|
||||||
|
if not val or key == "url"
|
||||||
|
continue
|
||||||
|
back.push("#{encodeURIComponent(key)}=#{encodeURIComponent(val)}")
|
||||||
|
return back.join("&")
|
||||||
|
|
||||||
|
highlight: (text, search) ->
|
||||||
|
if not text
|
||||||
|
return [""]
|
||||||
|
parts = text.split(RegExp(search, "i"))
|
||||||
|
back = []
|
||||||
|
for part, i in parts
|
||||||
|
back.push(part)
|
||||||
|
if i < parts.length-1
|
||||||
|
back.push(h("span.highlight", {key: i}, search))
|
||||||
|
return back
|
||||||
|
|
||||||
|
formatSize: (size) ->
|
||||||
|
if isNaN(parseInt(size))
|
||||||
|
return ""
|
||||||
|
size_mb = size/1024/1024
|
||||||
|
if size_mb >= 1000
|
||||||
|
return (size_mb/1024).toFixed(1)+" GB"
|
||||||
|
else if size_mb >= 100
|
||||||
|
return size_mb.toFixed(0)+" MB"
|
||||||
|
else if size/1024 >= 1000
|
||||||
|
return size_mb.toFixed(2)+" MB"
|
||||||
|
else
|
||||||
|
return (parseInt(size)/1024).toFixed(2)+" KB"
|
||||||
|
|
||||||
|
window.is_proxy = (document.location.host == "zero" or window.location.pathname == "/")
|
||||||
|
window.Text = new Text()
|
59
plugins/UiFileManager/media/js/lib/Time.coffee
Normal file
59
plugins/UiFileManager/media/js/lib/Time.coffee
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
class Time
|
||||||
|
since: (timestamp) ->
|
||||||
|
now = +(new Date)/1000
|
||||||
|
if timestamp > 1000000000000 # In ms
|
||||||
|
timestamp = timestamp/1000
|
||||||
|
secs = now - timestamp
|
||||||
|
if secs < 60
|
||||||
|
back = "Just now"
|
||||||
|
else if secs < 60*60
|
||||||
|
minutes = Math.round(secs/60)
|
||||||
|
back = "" + minutes + " minutes ago"
|
||||||
|
else if secs < 60*60*24
|
||||||
|
back = "#{Math.round(secs/60/60)} hours ago"
|
||||||
|
else if secs < 60*60*24*3
|
||||||
|
back = "#{Math.round(secs/60/60/24)} days ago"
|
||||||
|
else
|
||||||
|
back = "on "+@date(timestamp)
|
||||||
|
back = back.replace(/^1 ([a-z]+)s/, "1 $1") # 1 days ago fix
|
||||||
|
return back
|
||||||
|
|
||||||
|
dateIso: (timestamp=null) ->
|
||||||
|
if not timestamp
|
||||||
|
timestamp = window.Time.timestamp()
|
||||||
|
|
||||||
|
if timestamp > 1000000000000 # In ms
|
||||||
|
timestamp = timestamp/1000
|
||||||
|
tzoffset = (new Date()).getTimezoneOffset() * 60
|
||||||
|
return (new Date((timestamp - tzoffset) * 1000)).toISOString().split("T")[0]
|
||||||
|
|
||||||
|
date: (timestamp=null, format="short") ->
|
||||||
|
if not timestamp
|
||||||
|
timestamp = window.Time.timestamp()
|
||||||
|
|
||||||
|
if timestamp > 1000000000000 # In ms
|
||||||
|
timestamp = timestamp/1000
|
||||||
|
parts = (new Date(timestamp * 1000)).toString().split(" ")
|
||||||
|
if format == "short"
|
||||||
|
display = parts.slice(1, 4)
|
||||||
|
else if format == "day"
|
||||||
|
display = parts.slice(1, 3)
|
||||||
|
else if format == "month"
|
||||||
|
display = [parts[1], parts[3]]
|
||||||
|
else if format == "long"
|
||||||
|
display = parts.slice(1, 5)
|
||||||
|
return display.join(" ").replace(/( [0-9]{4})/, ",$1")
|
||||||
|
|
||||||
|
weekDay: (timestamp) ->
|
||||||
|
if timestamp > 1000000000000 # In ms
|
||||||
|
timestamp = timestamp/1000
|
||||||
|
return ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][ (new Date(timestamp * 1000)).getDay() ]
|
||||||
|
|
||||||
|
timestamp: (date="") ->
|
||||||
|
if date == "now" or date == ""
|
||||||
|
return parseInt(+(new Date)/1000)
|
||||||
|
else
|
||||||
|
return parseInt(Date.parse(date)/1000)
|
||||||
|
|
||||||
|
|
||||||
|
window.Time = new Time
|
85
plugins/UiFileManager/media/js/lib/ZeroFrame.coffee
Normal file
85
plugins/UiFileManager/media/js/lib/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
|
770
plugins/UiFileManager/media/js/lib/maquette.js
Normal file
770
plugins/UiFileManager/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;
|
||||||
|
};
|
||||||
|
}));
|
18
plugins/UiFileManager/media/list.html
Normal file
18
plugins/UiFileManager/media/list.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>List - ZeroNet</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||||
|
<link rel="stylesheet" href="css/all.css?site_modified={rev}" />
|
||||||
|
<base href="" target="_top" id="base">
|
||||||
|
<script>base.href = document.location.href.replace("/media", "").replace("index.html", "").replace(/[&?]wrapper=False/, "").replace(/[&?]wrapper_nonce=[A-Za-z0-9]+/, "")</script>
|
||||||
|
</head>
|
||||||
|
<body class="{themeclass}">
|
||||||
|
|
||||||
|
<div class="content" id="content"></div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="js/all.js?rev={rev}&lang={lang}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue