bring remote assets to local
This commit is contained in:
@ -10,23 +10,23 @@
|
|||||||
<meta name="theme-color" content="#1f2733">
|
<meta name="theme-color" content="#1f2733">
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="icon" href="/icon.png">
|
<link rel="icon" href="/icon.png">
|
||||||
<link rel="stylesheet" href="https://src.simplecyber.org/lib/codemirror5.css">
|
<link rel="stylesheet" href="https://sftp.my-mc.link/lib/codemirror5.css">
|
||||||
<link rel="stylesheet" href="https://src.simplecyber.org/v2/themes.css">
|
<link rel="stylesheet" href="https://sftp.my-mc.link/v2/themes.css">
|
||||||
<link rel="stylesheet" href="https://src.simplecyber.org/v2/base.css">
|
<link rel="stylesheet" href="https://sftp.my-mc.link/v2/base.css">
|
||||||
<link rel="stylesheet" href="/assets/main.css">
|
<link rel="stylesheet" href="/assets/main.css">
|
||||||
<script defer src="https://src.simplecyber.org/lib/axios.min.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/axios.min.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/tabbable.min.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/tabbable.min.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/focus-trap.min.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/focus-trap.min.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/dayjs.min.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/dayjs.min.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/marked.min.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/marked.min.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/codemirror5.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/codemirror5.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/codemirror5-scrollPastEnd.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/codemirror5-scrollPastEnd.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/codemirror5-activeLine.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/codemirror5-activeLine.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/codemirror5-loadMode.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/codemirror5-loadMode.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/codemirror5-closeBrackets.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/codemirror5-closeBrackets.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/codemirror5-overlay.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/codemirror5-overlay.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/v2/base.js"></script>
|
<script defer src="https://sftp.my-mc.link/v2/base.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/utils.js"></script>
|
<script defer src="https://sftp.my-mc.link/utils.js"></script>
|
||||||
<script defer src="/assets/main.js"></script>
|
<script defer src="/assets/main.js"></script>
|
||||||
<script defer src="/assets/file.js"></script>
|
<script defer src="/assets/file.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
BIN
web/fonts/Comfortaa-Bold.ttf
Normal file
BIN
web/fonts/Comfortaa-Bold.ttf
Normal file
Binary file not shown.
BIN
web/fonts/Comfortaa-Regular.ttf
Normal file
BIN
web/fonts/Comfortaa-Regular.ttf
Normal file
Binary file not shown.
13
web/fonts/cascadia-code.css
Normal file
13
web/fonts/cascadia-code.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Cascadia Code';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
src: url(https://src.simplecyber.org/fonts/CascadiaCode.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Cascadia Code';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 100 900;
|
||||||
|
src: url(https://src.simplecyber.org/fonts/CascadiaCodeItalic.ttf) format('truetype');
|
||||||
|
}
|
31
web/fonts/comfortaa.css
Normal file
31
web/fonts/comfortaa.css
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comfortaa';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url(/fonts/Comfortaa-Light.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comfortaa';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(/fonts/Comfortaa-Regular.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comfortaa';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url(/fonts/Comfortaa-Medium.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comfortaa';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
src: url(/fonts/Comfortaa-SemiBold.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Comfortaa';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url(/fonts/Comfortaa-Bold.ttf) format('truetype');
|
||||||
|
}
|
BIN
web/fonts/material-symbols-filled-rounded-300.woff2
Normal file
BIN
web/fonts/material-symbols-filled-rounded-300.woff2
Normal file
Binary file not shown.
BIN
web/fonts/material-symbols-outlined-rounded-300.woff2
Normal file
BIN
web/fonts/material-symbols-outlined-rounded-300.woff2
Normal file
Binary file not shown.
55
web/fonts/material-symbols-outlined.css
Normal file
55
web/fonts/material-symbols-outlined.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Outlined';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url(/fonts/material-symbols-outlined-300.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Outlined Rounded';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url(/fonts/material-symbols-outlined-rounded-300.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Filled';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url(/fonts/material-symbols-filled-300.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Filled Rounded';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url(/fonts/material-symbols-filled-rounded-300.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbol {
|
||||||
|
font-family: 'Material Symbols Outlined';
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: normal;
|
||||||
|
text-transform: none;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
word-wrap: normal;
|
||||||
|
direction: ltr;
|
||||||
|
-webkit-font-feature-settings: 'liga';
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.material-symbol.outlined {
|
||||||
|
font-family: 'Material Symbols Outlined';
|
||||||
|
}
|
||||||
|
.material-symbol.outlined.rounded {
|
||||||
|
font-family: 'Material Symbols Outlined Rounded';
|
||||||
|
}
|
||||||
|
.material-symbol.filled {
|
||||||
|
font-family: 'Material Symbols Filled';
|
||||||
|
}
|
||||||
|
.material-symbol.filled.rounded {
|
||||||
|
font-family: 'Material Symbols Filled Rounded';
|
||||||
|
}
|
55
web/fonts/poppins.css
Normal file
55
web/fonts/poppins.css
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100;
|
||||||
|
src: url(/fonts/Poppins-Thin.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 200;
|
||||||
|
src: url(/fonts/Poppins-ExtraLight.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: url(/fonts/Poppins-Light.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(/fonts/Poppins-Regular.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url(/fonts/Poppins-Medium.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
src: url(/fonts/Poppins-SemiBold.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url(/fonts/Poppins-Bold.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 800;
|
||||||
|
src: url(/fonts/Poppins-ExtraBold.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Poppins';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
src: url(/fonts/Poppins-Black.ttf) format('truetype');
|
||||||
|
}
|
@ -11,15 +11,15 @@
|
|||||||
<meta http-equiv="Permissions-Policy" content="clipboard-write=(self 'https://sftp.my-mc.link')">
|
<meta http-equiv="Permissions-Policy" content="clipboard-write=(self 'https://sftp.my-mc.link')">
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="icon" href="/icon.png">
|
<link rel="icon" href="/icon.png">
|
||||||
<link rel="stylesheet" href="https://src.simplecyber.org/v2/themes.css">
|
<link rel="stylesheet" href="https://sftp.my-mc.link/v2/themes.css">
|
||||||
<link rel="stylesheet" href="https://src.simplecyber.org/v2/base.css">
|
<link rel="stylesheet" href="https://sftp.my-mc.link/v2/base.css">
|
||||||
<link rel="stylesheet" href="/assets/main.css">
|
<link rel="stylesheet" href="/assets/main.css">
|
||||||
<script defer src="https://src.simplecyber.org/lib/axios.min.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/axios.min.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/tabbable.min.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/tabbable.min.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/focus-trap.min.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/focus-trap.min.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/lib/dayjs.min.js"></script>
|
<script defer src="https://sftp.my-mc.link/lib/dayjs.min.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/v2/base.js"></script>
|
<script defer src="https://sftp.my-mc.link/v2/base.js"></script>
|
||||||
<script defer src="https://src.simplecyber.org/utils.js"></script>
|
<script defer src="https://sftp.my-mc.link/utils.js"></script>
|
||||||
<script defer src="/assets/main.js"></script>
|
<script defer src="/assets/main.js"></script>
|
||||||
<script defer src="/assets/index.js"></script>
|
<script defer src="/assets/index.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
2
web/lib/axios.min.js
vendored
Normal file
2
web/lib/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
72
web/lib/codemirror5-activeLine.js
Normal file
72
web/lib/codemirror5-activeLine.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../../lib/codemirror"));
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../../lib/codemirror"], mod);
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror);
|
||||||
|
})(function(CodeMirror) {
|
||||||
|
"use strict";
|
||||||
|
var WRAP_CLASS = "CodeMirror-activeline";
|
||||||
|
var BACK_CLASS = "CodeMirror-activeline-background";
|
||||||
|
var GUTT_CLASS = "CodeMirror-activeline-gutter";
|
||||||
|
|
||||||
|
CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
|
||||||
|
var prev = old == CodeMirror.Init ? false : old;
|
||||||
|
if (val == prev) return
|
||||||
|
if (prev) {
|
||||||
|
cm.off("beforeSelectionChange", selectionChange);
|
||||||
|
clearActiveLines(cm);
|
||||||
|
delete cm.state.activeLines;
|
||||||
|
}
|
||||||
|
if (val) {
|
||||||
|
cm.state.activeLines = [];
|
||||||
|
updateActiveLines(cm, cm.listSelections());
|
||||||
|
cm.on("beforeSelectionChange", selectionChange);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function clearActiveLines(cm) {
|
||||||
|
for (var i = 0; i < cm.state.activeLines.length; i++) {
|
||||||
|
cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
|
||||||
|
cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
|
||||||
|
cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameArray(a, b) {
|
||||||
|
if (a.length != b.length) return false;
|
||||||
|
for (var i = 0; i < a.length; i++)
|
||||||
|
if (a[i] != b[i]) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateActiveLines(cm, ranges) {
|
||||||
|
var active = [];
|
||||||
|
for (var i = 0; i < ranges.length; i++) {
|
||||||
|
var range = ranges[i];
|
||||||
|
var option = cm.getOption("styleActiveLine");
|
||||||
|
if (typeof option == "object" && option.nonEmpty ? range.anchor.line != range.head.line : !range.empty())
|
||||||
|
continue
|
||||||
|
var line = cm.getLineHandleVisualStart(range.head.line);
|
||||||
|
if (active[active.length - 1] != line) active.push(line);
|
||||||
|
}
|
||||||
|
if (sameArray(cm.state.activeLines, active)) return;
|
||||||
|
cm.operation(function() {
|
||||||
|
clearActiveLines(cm);
|
||||||
|
for (var i = 0; i < active.length; i++) {
|
||||||
|
cm.addLineClass(active[i], "wrap", WRAP_CLASS);
|
||||||
|
cm.addLineClass(active[i], "background", BACK_CLASS);
|
||||||
|
cm.addLineClass(active[i], "gutter", GUTT_CLASS);
|
||||||
|
}
|
||||||
|
cm.state.activeLines = active;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectionChange(cm, sel) {
|
||||||
|
updateActiveLines(cm, sel.ranges);
|
||||||
|
}
|
||||||
|
});
|
201
web/lib/codemirror5-closeBrackets.js
Normal file
201
web/lib/codemirror5-closeBrackets.js
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../../lib/codemirror"));
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../../lib/codemirror"], mod);
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror);
|
||||||
|
})(function(CodeMirror) {
|
||||||
|
var defaults = {
|
||||||
|
pairs: "()[]{}''\"\"",
|
||||||
|
closeBefore: ")]}'\":;>",
|
||||||
|
triples: "",
|
||||||
|
explode: "[]{}"
|
||||||
|
};
|
||||||
|
|
||||||
|
var Pos = CodeMirror.Pos;
|
||||||
|
|
||||||
|
CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
|
||||||
|
if (old && old != CodeMirror.Init) {
|
||||||
|
cm.removeKeyMap(keyMap);
|
||||||
|
cm.state.closeBrackets = null;
|
||||||
|
}
|
||||||
|
if (val) {
|
||||||
|
ensureBound(getOption(val, "pairs"))
|
||||||
|
cm.state.closeBrackets = val;
|
||||||
|
cm.addKeyMap(keyMap);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getOption(conf, name) {
|
||||||
|
if (name == "pairs" && typeof conf == "string") return conf;
|
||||||
|
if (typeof conf == "object" && conf[name] != null) return conf[name];
|
||||||
|
return defaults[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyMap = {Backspace: handleBackspace, Enter: handleEnter};
|
||||||
|
function ensureBound(chars) {
|
||||||
|
for (var i = 0; i < chars.length; i++) {
|
||||||
|
var ch = chars.charAt(i), key = "'" + ch + "'"
|
||||||
|
if (!keyMap[key]) keyMap[key] = handler(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ensureBound(defaults.pairs + "`")
|
||||||
|
|
||||||
|
function handler(ch) {
|
||||||
|
return function(cm) { return handleChar(cm, ch); };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfig(cm) {
|
||||||
|
var deflt = cm.state.closeBrackets;
|
||||||
|
if (!deflt || deflt.override) return deflt;
|
||||||
|
var mode = cm.getModeAt(cm.getCursor());
|
||||||
|
return mode.closeBrackets || deflt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBackspace(cm) {
|
||||||
|
var conf = getConfig(cm);
|
||||||
|
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||||
|
|
||||||
|
var pairs = getOption(conf, "pairs");
|
||||||
|
var ranges = cm.listSelections();
|
||||||
|
for (var i = 0; i < ranges.length; i++) {
|
||||||
|
if (!ranges[i].empty()) return CodeMirror.Pass;
|
||||||
|
var around = charsAround(cm, ranges[i].head);
|
||||||
|
if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
|
||||||
|
}
|
||||||
|
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||||
|
var cur = ranges[i].head;
|
||||||
|
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEnter(cm) {
|
||||||
|
var conf = getConfig(cm);
|
||||||
|
var explode = conf && getOption(conf, "explode");
|
||||||
|
if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||||
|
|
||||||
|
var ranges = cm.listSelections();
|
||||||
|
for (var i = 0; i < ranges.length; i++) {
|
||||||
|
if (!ranges[i].empty()) return CodeMirror.Pass;
|
||||||
|
var around = charsAround(cm, ranges[i].head);
|
||||||
|
if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
|
||||||
|
}
|
||||||
|
cm.operation(function() {
|
||||||
|
var linesep = cm.lineSeparator() || "\n";
|
||||||
|
cm.replaceSelection(linesep + linesep, null);
|
||||||
|
moveSel(cm, -1)
|
||||||
|
ranges = cm.listSelections();
|
||||||
|
for (var i = 0; i < ranges.length; i++) {
|
||||||
|
var line = ranges[i].head.line;
|
||||||
|
cm.indentLine(line, null, true);
|
||||||
|
cm.indentLine(line + 1, null, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveSel(cm, dir) {
|
||||||
|
var newRanges = [], ranges = cm.listSelections(), primary = 0
|
||||||
|
for (var i = 0; i < ranges.length; i++) {
|
||||||
|
var range = ranges[i]
|
||||||
|
if (range.head == cm.getCursor()) primary = i
|
||||||
|
var pos = range.head.ch || dir > 0 ? {line: range.head.line, ch: range.head.ch + dir} : {line: range.head.line - 1}
|
||||||
|
newRanges.push({anchor: pos, head: pos})
|
||||||
|
}
|
||||||
|
cm.setSelections(newRanges, primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
function contractSelection(sel) {
|
||||||
|
var inverted = CodeMirror.cmpPos(sel.anchor, sel.head) > 0;
|
||||||
|
return {anchor: new Pos(sel.anchor.line, sel.anchor.ch + (inverted ? -1 : 1)),
|
||||||
|
head: new Pos(sel.head.line, sel.head.ch + (inverted ? 1 : -1))};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChar(cm, ch) {
|
||||||
|
var conf = getConfig(cm);
|
||||||
|
if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||||
|
|
||||||
|
var pairs = getOption(conf, "pairs");
|
||||||
|
var pos = pairs.indexOf(ch);
|
||||||
|
if (pos == -1) return CodeMirror.Pass;
|
||||||
|
|
||||||
|
var closeBefore = getOption(conf,"closeBefore");
|
||||||
|
|
||||||
|
var triples = getOption(conf, "triples");
|
||||||
|
|
||||||
|
var identical = pairs.charAt(pos + 1) == ch;
|
||||||
|
var ranges = cm.listSelections();
|
||||||
|
var opening = pos % 2 == 0;
|
||||||
|
|
||||||
|
var type;
|
||||||
|
for (var i = 0; i < ranges.length; i++) {
|
||||||
|
var range = ranges[i], cur = range.head, curType;
|
||||||
|
var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
|
||||||
|
if (opening && !range.empty()) {
|
||||||
|
curType = "surround";
|
||||||
|
} else if ((identical || !opening) && next == ch) {
|
||||||
|
if (identical && stringStartsAfter(cm, cur))
|
||||||
|
curType = "both";
|
||||||
|
else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
|
||||||
|
curType = "skipThree";
|
||||||
|
else
|
||||||
|
curType = "skip";
|
||||||
|
} else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
|
||||||
|
cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch) {
|
||||||
|
if (cur.ch > 2 && /\bstring/.test(cm.getTokenTypeAt(Pos(cur.line, cur.ch - 2)))) return CodeMirror.Pass;
|
||||||
|
curType = "addFour";
|
||||||
|
} else if (identical) {
|
||||||
|
var prev = cur.ch == 0 ? " " : cm.getRange(Pos(cur.line, cur.ch - 1), cur)
|
||||||
|
if (!CodeMirror.isWordChar(next) && prev != ch && !CodeMirror.isWordChar(prev)) curType = "both";
|
||||||
|
else return CodeMirror.Pass;
|
||||||
|
} else if (opening && (next.length === 0 || /\s/.test(next) || closeBefore.indexOf(next) > -1)) {
|
||||||
|
curType = "both";
|
||||||
|
} else {
|
||||||
|
return CodeMirror.Pass;
|
||||||
|
}
|
||||||
|
if (!type) type = curType;
|
||||||
|
else if (type != curType) return CodeMirror.Pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
var left = pos % 2 ? pairs.charAt(pos - 1) : ch;
|
||||||
|
var right = pos % 2 ? ch : pairs.charAt(pos + 1);
|
||||||
|
cm.operation(function() {
|
||||||
|
if (type == "skip") {
|
||||||
|
moveSel(cm, 1)
|
||||||
|
} else if (type == "skipThree") {
|
||||||
|
moveSel(cm, 3)
|
||||||
|
} else if (type == "surround") {
|
||||||
|
var sels = cm.getSelections();
|
||||||
|
for (var i = 0; i < sels.length; i++)
|
||||||
|
sels[i] = left + sels[i] + right;
|
||||||
|
cm.replaceSelections(sels, "around");
|
||||||
|
sels = cm.listSelections().slice();
|
||||||
|
for (var i = 0; i < sels.length; i++)
|
||||||
|
sels[i] = contractSelection(sels[i]);
|
||||||
|
cm.setSelections(sels);
|
||||||
|
} else if (type == "both") {
|
||||||
|
cm.replaceSelection(left + right, null);
|
||||||
|
cm.triggerElectric(left + right);
|
||||||
|
moveSel(cm, -1)
|
||||||
|
} else if (type == "addFour") {
|
||||||
|
cm.replaceSelection(left + left + left + left, "before");
|
||||||
|
moveSel(cm, 1)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function charsAround(cm, pos) {
|
||||||
|
var str = cm.getRange(Pos(pos.line, pos.ch - 1),
|
||||||
|
Pos(pos.line, pos.ch + 1));
|
||||||
|
return str.length == 2 ? str : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringStartsAfter(cm, pos) {
|
||||||
|
var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
|
||||||
|
return /\bstring/.test(token.type) && token.start == pos.ch &&
|
||||||
|
(pos.ch == 0 || !/\bstring/.test(cm.getTokenTypeAt(pos)))
|
||||||
|
}
|
||||||
|
});
|
66
web/lib/codemirror5-loadMode.js
Normal file
66
web/lib/codemirror5-loadMode.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../../lib/codemirror"), "cjs");
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../../lib/codemirror"], function(CM) { mod(CM, "amd"); });
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror, "plain");
|
||||||
|
})(function(CodeMirror, env) {
|
||||||
|
if (!CodeMirror.modeURL) CodeMirror.modeURL = "../mode/%N/%N.js";
|
||||||
|
|
||||||
|
var loading = {};
|
||||||
|
function splitCallback(cont, n) {
|
||||||
|
var countDown = n;
|
||||||
|
return function() { if (--countDown == 0) cont(); };
|
||||||
|
}
|
||||||
|
function ensureDeps(mode, cont, options) {
|
||||||
|
var modeObj = CodeMirror.modes[mode], deps = modeObj && modeObj.dependencies;
|
||||||
|
if (!deps) return cont();
|
||||||
|
var missing = [];
|
||||||
|
for (var i = 0; i < deps.length; ++i) {
|
||||||
|
if (!CodeMirror.modes.hasOwnProperty(deps[i]))
|
||||||
|
missing.push(deps[i]);
|
||||||
|
}
|
||||||
|
if (!missing.length) return cont();
|
||||||
|
var split = splitCallback(cont, missing.length);
|
||||||
|
for (var i = 0; i < missing.length; ++i)
|
||||||
|
CodeMirror.requireMode(missing[i], split, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
CodeMirror.requireMode = function(mode, cont, options) {
|
||||||
|
if (typeof mode != "string") mode = mode.name;
|
||||||
|
if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont, options);
|
||||||
|
if (loading.hasOwnProperty(mode)) return loading[mode].push(cont);
|
||||||
|
|
||||||
|
var file = options && options.path ? options.path(mode) : CodeMirror.modeURL.replace(/%N/g, mode);
|
||||||
|
if (options && options.loadMode) {
|
||||||
|
options.loadMode(file, function() { ensureDeps(mode, cont, options) })
|
||||||
|
} else if (env == "plain") {
|
||||||
|
var script = document.createElement("script");
|
||||||
|
script.src = file;
|
||||||
|
var others = document.getElementsByTagName("script")[0];
|
||||||
|
var list = loading[mode] = [cont];
|
||||||
|
CodeMirror.on(script, "load", function() {
|
||||||
|
ensureDeps(mode, function() {
|
||||||
|
for (var i = 0; i < list.length; ++i) list[i]();
|
||||||
|
}, options);
|
||||||
|
});
|
||||||
|
others.parentNode.insertBefore(script, others);
|
||||||
|
} else if (env == "cjs") {
|
||||||
|
require(file);
|
||||||
|
cont();
|
||||||
|
} else if (env == "amd") {
|
||||||
|
requirejs([file], cont);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CodeMirror.autoLoadMode = function(instance, mode, options) {
|
||||||
|
if (!CodeMirror.modes.hasOwnProperty(mode))
|
||||||
|
CodeMirror.requireMode(mode, function() {
|
||||||
|
instance.setOption("mode", instance.getOption("mode"));
|
||||||
|
}, options);
|
||||||
|
};
|
||||||
|
});
|
90
web/lib/codemirror5-overlay.js
Normal file
90
web/lib/codemirror5-overlay.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||||
|
|
||||||
|
// Utility function that allows modes to be combined. The mode given
|
||||||
|
// as the base argument takes care of most of the normal mode
|
||||||
|
// functionality, but a second (typically simple) mode is used, which
|
||||||
|
// can override the style of text. Both modes get to parse all of the
|
||||||
|
// text, but when both assign a non-null style to a piece of code, the
|
||||||
|
// overlay wins, unless the combine argument was true and not overridden,
|
||||||
|
// or state.overlay.combineTokens was true, in which case the styles are
|
||||||
|
// combined.
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../../lib/codemirror"));
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../../lib/codemirror"], mod);
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror);
|
||||||
|
})(function(CodeMirror) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
CodeMirror.overlayMode = function(base, overlay, combine) {
|
||||||
|
return {
|
||||||
|
startState: function() {
|
||||||
|
return {
|
||||||
|
base: CodeMirror.startState(base),
|
||||||
|
overlay: CodeMirror.startState(overlay),
|
||||||
|
basePos: 0, baseCur: null,
|
||||||
|
overlayPos: 0, overlayCur: null,
|
||||||
|
streamSeen: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
copyState: function(state) {
|
||||||
|
return {
|
||||||
|
base: CodeMirror.copyState(base, state.base),
|
||||||
|
overlay: CodeMirror.copyState(overlay, state.overlay),
|
||||||
|
basePos: state.basePos, baseCur: null,
|
||||||
|
overlayPos: state.overlayPos, overlayCur: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
token: function(stream, state) {
|
||||||
|
if (stream != state.streamSeen ||
|
||||||
|
Math.min(state.basePos, state.overlayPos) < stream.start) {
|
||||||
|
state.streamSeen = stream;
|
||||||
|
state.basePos = state.overlayPos = stream.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream.start == state.basePos) {
|
||||||
|
state.baseCur = base.token(stream, state.base);
|
||||||
|
state.basePos = stream.pos;
|
||||||
|
}
|
||||||
|
if (stream.start == state.overlayPos) {
|
||||||
|
stream.pos = stream.start;
|
||||||
|
state.overlayCur = overlay.token(stream, state.overlay);
|
||||||
|
state.overlayPos = stream.pos;
|
||||||
|
}
|
||||||
|
stream.pos = Math.min(state.basePos, state.overlayPos);
|
||||||
|
|
||||||
|
// state.overlay.combineTokens always takes precedence over combine,
|
||||||
|
// unless set to null
|
||||||
|
if (state.overlayCur == null) return state.baseCur;
|
||||||
|
else if (state.baseCur != null &&
|
||||||
|
state.overlay.combineTokens ||
|
||||||
|
combine && state.overlay.combineTokens == null)
|
||||||
|
return state.baseCur + " " + state.overlayCur;
|
||||||
|
else return state.overlayCur;
|
||||||
|
},
|
||||||
|
|
||||||
|
indent: base.indent && function(state, textAfter, line) {
|
||||||
|
return base.indent(state.base, textAfter, line);
|
||||||
|
},
|
||||||
|
electricChars: base.electricChars,
|
||||||
|
|
||||||
|
innerMode: function(state) { return {state: state.base, mode: base}; },
|
||||||
|
|
||||||
|
blankLine: function(state) {
|
||||||
|
var baseToken, overlayToken;
|
||||||
|
if (base.blankLine) baseToken = base.blankLine(state.base);
|
||||||
|
if (overlay.blankLine) overlayToken = overlay.blankLine(state.overlay);
|
||||||
|
|
||||||
|
return overlayToken == null ?
|
||||||
|
baseToken :
|
||||||
|
(combine && baseToken != null ? baseToken + " " + overlayToken : overlayToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
48
web/lib/codemirror5-scrollPastEnd.js
Normal file
48
web/lib/codemirror5-scrollPastEnd.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
|
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||||
|
|
||||||
|
(function(mod) {
|
||||||
|
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||||
|
mod(require("../../lib/codemirror"));
|
||||||
|
else if (typeof define == "function" && define.amd) // AMD
|
||||||
|
define(["../../lib/codemirror"], mod);
|
||||||
|
else // Plain browser env
|
||||||
|
mod(CodeMirror);
|
||||||
|
})(function(CodeMirror) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
CodeMirror.defineOption("scrollPastEnd", false, function(cm, val, old) {
|
||||||
|
if (old && old != CodeMirror.Init) {
|
||||||
|
cm.off("change", onChange);
|
||||||
|
cm.off("refresh", updateBottomMargin);
|
||||||
|
cm.display.lineSpace.parentNode.style.paddingBottom = "";
|
||||||
|
cm.state.scrollPastEndPadding = null;
|
||||||
|
}
|
||||||
|
if (val) {
|
||||||
|
cm.on("change", onChange);
|
||||||
|
cm.on("refresh", updateBottomMargin);
|
||||||
|
updateBottomMargin(cm);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onChange(cm, change) {
|
||||||
|
if (CodeMirror.changeEnd(change).line == cm.lastLine())
|
||||||
|
updateBottomMargin(cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBottomMargin(cm) {
|
||||||
|
var padding = "";
|
||||||
|
if (cm.lineCount() > 1) {
|
||||||
|
var totalH = cm.display.scroller.clientHeight - 30,
|
||||||
|
lastLineH = cm.getLineHandle(cm.lastLine()).height;
|
||||||
|
padding = (totalH - lastLineH) + "px";
|
||||||
|
}
|
||||||
|
if (cm.state.scrollPastEndPadding != padding) {
|
||||||
|
cm.state.scrollPastEndPadding = padding;
|
||||||
|
cm.display.lineSpace.parentNode.style.paddingBottom = padding;
|
||||||
|
cm.off("refresh", updateBottomMargin);
|
||||||
|
cm.setSize();
|
||||||
|
cm.on("refresh", updateBottomMargin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
344
web/lib/codemirror5.css
Normal file
344
web/lib/codemirror5.css
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
/* BASICS */
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
/* Set height, width, borders, and global font properties here */
|
||||||
|
font-family: monospace;
|
||||||
|
height: 300px;
|
||||||
|
color: black;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PADDING */
|
||||||
|
|
||||||
|
.CodeMirror-lines {
|
||||||
|
padding: 4px 0; /* Vertical padding around content */
|
||||||
|
}
|
||||||
|
.CodeMirror pre.CodeMirror-line,
|
||||||
|
.CodeMirror pre.CodeMirror-line-like {
|
||||||
|
padding: 0 4px; /* Horizontal padding of content */
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||||
|
background-color: white; /* The little square between H and V scrollbars */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GUTTER */
|
||||||
|
|
||||||
|
.CodeMirror-gutters {
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.CodeMirror-linenumbers {}
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
padding: 0 3px 0 5px;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: right;
|
||||||
|
color: #999;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-guttermarker { color: black; }
|
||||||
|
.CodeMirror-guttermarker-subtle { color: #999; }
|
||||||
|
|
||||||
|
/* CURSOR */
|
||||||
|
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
border-left: 1px solid black;
|
||||||
|
border-right: none;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
/* Shown when moving in bi-directional text */
|
||||||
|
.CodeMirror div.CodeMirror-secondarycursor {
|
||||||
|
border-left: 1px solid silver;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor .CodeMirror-cursor {
|
||||||
|
width: auto;
|
||||||
|
border: 0 !important;
|
||||||
|
background: #7e7;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor div.CodeMirror-cursors {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.cm-fat-cursor .CodeMirror-line::selection,
|
||||||
|
.cm-fat-cursor .CodeMirror-line > span::selection,
|
||||||
|
.cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
|
||||||
|
.cm-fat-cursor .CodeMirror-line::-moz-selection,
|
||||||
|
.cm-fat-cursor .CodeMirror-line > span::-moz-selection,
|
||||||
|
.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
|
||||||
|
.cm-fat-cursor { caret-color: transparent; }
|
||||||
|
@-moz-keyframes blink {
|
||||||
|
0% {}
|
||||||
|
50% { background-color: transparent; }
|
||||||
|
100% {}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes blink {
|
||||||
|
0% {}
|
||||||
|
50% { background-color: transparent; }
|
||||||
|
100% {}
|
||||||
|
}
|
||||||
|
@keyframes blink {
|
||||||
|
0% {}
|
||||||
|
50% { background-color: transparent; }
|
||||||
|
100% {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Can style cursor different in overwrite (non-insert) mode */
|
||||||
|
.CodeMirror-overwrite .CodeMirror-cursor {}
|
||||||
|
|
||||||
|
.cm-tab { display: inline-block; text-decoration: inherit; }
|
||||||
|
|
||||||
|
.CodeMirror-rulers {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: -50px; bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.CodeMirror-ruler {
|
||||||
|
border-left: 1px solid #ccc;
|
||||||
|
top: 0; bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DEFAULT THEME */
|
||||||
|
|
||||||
|
.cm-s-default .cm-header {color: blue;}
|
||||||
|
.cm-s-default .cm-quote {color: #090;}
|
||||||
|
.cm-negative {color: #d44;}
|
||||||
|
.cm-positive {color: #292;}
|
||||||
|
.cm-header, .cm-strong {font-weight: bold;}
|
||||||
|
.cm-em {font-style: italic;}
|
||||||
|
.cm-link {text-decoration: underline;}
|
||||||
|
.cm-strikethrough {text-decoration: line-through;}
|
||||||
|
|
||||||
|
.cm-s-default .cm-keyword {color: #708;}
|
||||||
|
.cm-s-default .cm-atom {color: #219;}
|
||||||
|
.cm-s-default .cm-number {color: #164;}
|
||||||
|
.cm-s-default .cm-def {color: #00f;}
|
||||||
|
.cm-s-default .cm-variable,
|
||||||
|
.cm-s-default .cm-punctuation,
|
||||||
|
.cm-s-default .cm-property,
|
||||||
|
.cm-s-default .cm-operator {}
|
||||||
|
.cm-s-default .cm-variable-2 {color: #05a;}
|
||||||
|
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
|
||||||
|
.cm-s-default .cm-comment {color: #a50;}
|
||||||
|
.cm-s-default .cm-string {color: #a11;}
|
||||||
|
.cm-s-default .cm-string-2 {color: #f50;}
|
||||||
|
.cm-s-default .cm-meta {color: #555;}
|
||||||
|
.cm-s-default .cm-qualifier {color: #555;}
|
||||||
|
.cm-s-default .cm-builtin {color: #30a;}
|
||||||
|
.cm-s-default .cm-bracket {color: #997;}
|
||||||
|
.cm-s-default .cm-tag {color: #170;}
|
||||||
|
.cm-s-default .cm-attribute {color: #00c;}
|
||||||
|
.cm-s-default .cm-hr {color: #999;}
|
||||||
|
.cm-s-default .cm-link {color: #00c;}
|
||||||
|
|
||||||
|
.cm-s-default .cm-error {color: #f00;}
|
||||||
|
.cm-invalidchar {color: #f00;}
|
||||||
|
|
||||||
|
.CodeMirror-composing { border-bottom: 2px solid; }
|
||||||
|
|
||||||
|
/* Default styles for common addons */
|
||||||
|
|
||||||
|
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
|
||||||
|
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
|
||||||
|
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
|
||||||
|
.CodeMirror-activeline-background {background: #e8f2ff;}
|
||||||
|
|
||||||
|
/* STOP */
|
||||||
|
|
||||||
|
/* The rest of this file contains styles related to the mechanics of
|
||||||
|
the editor. You probably shouldn't touch them. */
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-scroll {
|
||||||
|
overflow: scroll !important; /* Things will break if this is overridden */
|
||||||
|
/* 50px is the magic margin used to hide the element's real scrollbars */
|
||||||
|
/* See overflow: hidden in .CodeMirror */
|
||||||
|
margin-bottom: -50px; margin-right: -50px;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
height: 100%;
|
||||||
|
outline: none; /* Prevent dragging from highlighting the element */
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.CodeMirror-sizer {
|
||||||
|
position: relative;
|
||||||
|
border-right: 50px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||||
|
before actual scrolling happens, thus preventing shaking and
|
||||||
|
flickering artifacts. */
|
||||||
|
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 6;
|
||||||
|
display: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.CodeMirror-vscrollbar {
|
||||||
|
right: 0; top: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
.CodeMirror-hscrollbar {
|
||||||
|
bottom: 0; left: 0;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
.CodeMirror-scrollbar-filler {
|
||||||
|
right: 0; bottom: 0;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-filler {
|
||||||
|
left: 0; bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-gutters {
|
||||||
|
position: absolute; left: 0; top: 0;
|
||||||
|
min-height: 100%;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter {
|
||||||
|
white-space: normal;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-bottom: -50px;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
background: none !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; bottom: 0;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-elt {
|
||||||
|
position: absolute;
|
||||||
|
cursor: default;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
|
||||||
|
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
|
||||||
|
|
||||||
|
.CodeMirror-lines {
|
||||||
|
cursor: text;
|
||||||
|
min-height: 1px; /* prevents collapsing before first draw */
|
||||||
|
}
|
||||||
|
.CodeMirror pre.CodeMirror-line,
|
||||||
|
.CodeMirror pre.CodeMirror-line-like {
|
||||||
|
/* Reset some styles that the rest of the page might have set */
|
||||||
|
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||||
|
border-width: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
z-index: 2;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
-webkit-font-variant-ligatures: contextual;
|
||||||
|
font-variant-ligatures: contextual;
|
||||||
|
}
|
||||||
|
.CodeMirror-wrap pre.CodeMirror-line,
|
||||||
|
.CodeMirror-wrap pre.CodeMirror-line-like {
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linebackground {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: 0; bottom: 0;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-linewidget {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 0.1px; /* Force widget margins to stay inside of the container */
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-widget {}
|
||||||
|
|
||||||
|
.CodeMirror-rtl pre { direction: rtl; }
|
||||||
|
|
||||||
|
.CodeMirror-code {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force content-box sizing for the elements where we expect it */
|
||||||
|
.CodeMirror-scroll,
|
||||||
|
.CodeMirror-sizer,
|
||||||
|
.CodeMirror-gutter,
|
||||||
|
.CodeMirror-gutters,
|
||||||
|
.CodeMirror-linenumber {
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-measure {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.CodeMirror-measure pre { position: static; }
|
||||||
|
|
||||||
|
div.CodeMirror-cursors {
|
||||||
|
visibility: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
div.CodeMirror-dragcursors {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-focused div.CodeMirror-cursors {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-selected { background: #d9d9d9; }
|
||||||
|
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
|
||||||
|
.CodeMirror-crosshair { cursor: crosshair; }
|
||||||
|
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
|
||||||
|
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
|
||||||
|
|
||||||
|
.cm-searching {
|
||||||
|
background-color: #ffa;
|
||||||
|
background-color: rgba(255, 255, 0, .4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Used to force a border model for a node */
|
||||||
|
.cm-force-border { padding-right: .1px; }
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Hide the cursor when printing */
|
||||||
|
.CodeMirror div.CodeMirror-cursors {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* See issue #2901 */
|
||||||
|
.cm-tab-wrap-hack:after { content: ''; }
|
||||||
|
|
||||||
|
/* Help users use markselection to safely style text background */
|
||||||
|
span.CodeMirror-selectedtext { background: none; }
|
9874
web/lib/codemirror5.js
Normal file
9874
web/lib/codemirror5.js
Normal file
File diff suppressed because it is too large
Load Diff
1
web/lib/dayjs.min.js
vendored
Normal file
1
web/lib/dayjs.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
web/lib/focus-trap.min.js
vendored
Normal file
4
web/lib/focus-trap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
web/lib/marked.min.js
vendored
Normal file
6
web/lib/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
web/lib/tabbable.min.js
vendored
Normal file
4
web/lib/tabbable.min.js
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/*!
|
||||||
|
* tabbable 5.3.2
|
||||||
|
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
|
||||||
|
*/ !function(e,t){var n,o;"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):(n=(e="undefined"!=typeof globalThis?globalThis:e||self).tabbable,t(o=e.tabbable={}),o.noConflict=function(){return e.tabbable=n,o})}(this,function(e){"use strict";var t=["input","select","textarea","a[href]","button","[tabindex]:not(slot)","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])',"details>summary:first-of-type","details"],n=t.join(","),o="undefined"==typeof Element,r=o?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector,a=!o&&Element.prototype.getRootNode?function(e){return e.getRootNode()}:function(e){return e.ownerDocument},i=function e(t,o,a){var i=Array.prototype.slice.apply(t.querySelectorAll(n));return o&&r.call(t,n)&&i.unshift(t),i=i.filter(a)},u=function e(t,o,a){for(var i=[],u=Array.from(t);u.length;){var l=u.shift();if("SLOT"===l.tagName){var c=l.assignedElements(),d=c.length?c:l.children,f=e(d,!0,a);a.flatten?i.push.apply(i,f):i.push({scope:l,candidates:f})}else{r.call(l,n)&&a.filter(l)&&(o||!t.includes(l))&&i.push(l);var s=l.shadowRoot||"function"==typeof a.getShadowRoot&&a.getShadowRoot(l);if(s){var p=e(!0===s?l.children:s.children,!0,a);a.flatten?i.push.apply(i,p):i.push({scope:l,candidates:p})}else u.unshift.apply(u,l.children)}}return i},l=function e(t,n){return t.tabIndex<0&&(n||/^(AUDIO|VIDEO|DETAILS)$/.test(t.tagName)||t.isContentEditable)&&isNaN(parseInt(t.getAttribute("tabindex"),10))?0:t.tabIndex},c=function e(t,n){return t.tabIndex===n.tabIndex?t.documentOrder-n.documentOrder:t.tabIndex-n.tabIndex},d=function e(t){return"INPUT"===t.tagName},f=function e(t,n){for(var o=0;o<t.length;o++)if(t[o].checked&&t[o].form===n)return t[o]},s=function e(t){if(!t.name)return!0;var n,o=t.form||a(t),r=function e(t){return o.querySelectorAll('input[type="radio"][name="'+t+'"]')};if("undefined"!=typeof window&&void 0!==window.CSS&&"function"==typeof window.CSS.escape)n=r(window.CSS.escape(t.name));else try{n=r(t.name)}catch(i){return console.error("Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s",i.message),!1}var u=f(n,t.form);return!u||u===t},p=function e(t){var n;return d(n=t)&&"radio"===n.type&&!s(t)},h=function e(t){var n=t.getBoundingClientRect(),o=n.width,r=n.height;return 0===o&&0===r},b=function e(t,n){var o=n.displayCheck,i=n.getShadowRoot;if("hidden"===getComputedStyle(t).visibility)return!0;var u=r.call(t,"details>summary:first-of-type")?t.parentElement:t;if(r.call(u,"details:not([open]) *"))return!0;var l=a(t).host,c=(null==l?void 0:l.ownerDocument.contains(l))||t.ownerDocument.contains(t);if(o&&"full"!==o){if("non-zero-area"===o)return h(t)}else{if("function"==typeof i){for(var d=t;t;){var f=t.parentElement,s=a(t);if(f&&!f.shadowRoot&&!0===i(f))return h(t);t=t.assignedSlot?t.assignedSlot:f||s===t.ownerDocument?f:s.host}t=d}if(c)return!t.getClientRects().length}return!1},m=function e(t){if(/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(t.tagName))for(var n=t.parentElement;n;){if("FIELDSET"===n.tagName&&n.disabled){for(var o=0;o<n.children.length;o++){var a=n.children.item(o);if("LEGEND"===a.tagName)return!!r.call(n,"fieldset[disabled] *")||!a.contains(t)}return!0}n=n.parentElement}return!1},y=function e(t,n){var o,r;return!(n.disabled||d(o=n)&&"hidden"===o.type||b(n,t)||(r=n,"DETAILS"===r.tagName&&Array.prototype.slice.apply(r.children).some(function(e){return"SUMMARY"===e.tagName}))||m(n))},v=function e(t,n){return!(p(n)||0>l(n))&&!!y(t,n)},g=function e(t){var n=[],o=[];return t.forEach(function(t,r){var a=!!t.scope,i=a?t.scope:t,u=l(i,a),c=a?e(t.candidates):i;0===u?a?n.push.apply(n,c):n.push(i):o.push({documentOrder:r,tabIndex:u,item:t,isScope:a,content:c})}),o.sort(c).reduce(function(e,t){return t.isScope?e.push.apply(e,t.content):e.push(t.content),e},[]).concat(n)},S=function e(t,n){var o;return o=(n=n||{}).getShadowRoot?u([t],n.includeContainer,{filter:v.bind(null,n),flatten:!1,getShadowRoot:n.getShadowRoot}):i(t,n.includeContainer,v.bind(null,n)),g(o)},$=function e(t,n){var o;return(n=n||{}).getShadowRoot?u([t],n.includeContainer,{filter:y.bind(null,n),flatten:!0,getShadowRoot:n.getShadowRoot}):i(t,n.includeContainer,y.bind(null,n))},w=function e(t,o){if(o=o||{},!t)throw Error("No node provided");return!1!==r.call(t,n)&&v(o,t)},E=t.concat("iframe").join(","),R=function e(t,n){if(n=n||{},!t)throw Error("No node provided");return!1!==r.call(t,E)&&y(n,t)};e.focusable=$,e.isFocusable=R,e.isTabbable=w,e.tabbable=S,Object.defineProperty(e,"__esModule",{value:!0})});
|
323
web/utils.js
Normal file
323
web/utils.js
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random string of a desired length, containing any of the desired characters.
|
||||||
|
* @param {number} length The resulting string's length
|
||||||
|
* @param {string|string[]} chars A list of possible characters to use
|
||||||
|
* @returns {string} The resulting random string
|
||||||
|
*/
|
||||||
|
function randomString(length, chars = '0123456789abcdefghijklmnopqrstuvwxyz') {
|
||||||
|
let str = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
str += chars[randomInt(0, chars.length-1)];
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a lowercase hexadecimal string of a desired length.
|
||||||
|
* @param {number} length The length of the random
|
||||||
|
* @returns {String} The resulting string
|
||||||
|
*/
|
||||||
|
function randomHex(length = 8) {
|
||||||
|
return randomString(length, '0123456789abcdef');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a pseudorandom integer between a minimum and maximum.
|
||||||
|
* @param {number} min The minimum
|
||||||
|
* @param {number} max The maximum
|
||||||
|
* @returns {number} The resulting integer
|
||||||
|
*/
|
||||||
|
function randomInt(min, max) {
|
||||||
|
return Math.round(min+(Math.random()*(max-min)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a pseudorandom float between a minimum and maximum.
|
||||||
|
* @param {number} min The minimum
|
||||||
|
* @param {number} max The maximum
|
||||||
|
* @returns {number} The resulting float
|
||||||
|
*/
|
||||||
|
function randomFloat(min, max) {
|
||||||
|
return min+(Math.random()*(max-min));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects a random element from an array.
|
||||||
|
* @param {array} arr The input array
|
||||||
|
* @returns {*} A randomly selected array element
|
||||||
|
*/
|
||||||
|
const getRandomElement = (arr) => arr[(Math.ceil(Math.random()*arr.length)-1)];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef itemWithWeight
|
||||||
|
* @property {*} value The return value for this item
|
||||||
|
* @property {number} weight The weight of this item
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Selects a random item from a provided list, where each item has a specific weight.
|
||||||
|
* @param {itemWithWeight[]} args An array of items and their weights
|
||||||
|
* @returns A randomly selected value
|
||||||
|
*/
|
||||||
|
const getRandomWeighted = (args) => {
|
||||||
|
// Get the total weight
|
||||||
|
let total = 0;
|
||||||
|
args.forEach((arg) => {
|
||||||
|
total += arg.weight;
|
||||||
|
});
|
||||||
|
// Sort options from lightest to heaviest
|
||||||
|
args.sort((a, b) => {
|
||||||
|
return a.weight-b.weight;
|
||||||
|
});
|
||||||
|
// Get our random number
|
||||||
|
const rand = randomFloat(0, total);
|
||||||
|
// Iterate through options until the current weight (num)
|
||||||
|
// is greater than our number (rand)
|
||||||
|
let num = 0;
|
||||||
|
for (const arg of args) {
|
||||||
|
num += arg.weight;
|
||||||
|
if (rand < num) return arg.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps a number within a range by preventing it from going above/below its maximum/minimum.
|
||||||
|
* @param {number} num The input number
|
||||||
|
* @param {number} min The minimum number
|
||||||
|
* @param {number} max The maximum number
|
||||||
|
* @returns {number} The resulting number
|
||||||
|
*/
|
||||||
|
function clamp(num, min, max) {
|
||||||
|
if (num < min) return min;
|
||||||
|
if (num > max) return max;
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps a number within a range by underflowing/overflowing.
|
||||||
|
* @param {number} num The input number
|
||||||
|
* @param {number} min The minimum number
|
||||||
|
* @param {number} max The maximum number
|
||||||
|
* @returns {number} The resulting number
|
||||||
|
*/
|
||||||
|
function overflow(num, min, max) {
|
||||||
|
if (num < min) return max;
|
||||||
|
if (num > max) return min;
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds a float to the desired amount of decimal places, clipping any trailing zeros.
|
||||||
|
* @param {number} number The input number
|
||||||
|
* @param {number} decimalPlaces The maximum number of decimal places
|
||||||
|
* @returns {number} The resulting number
|
||||||
|
*/
|
||||||
|
function roundSmart(number, decimalPlaces = 0) {
|
||||||
|
const factorOfTen = Math.pow(10, decimalPlaces);
|
||||||
|
return Math.round(number * factorOfTen) / factorOfTen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separates a string into its words and returns an array of those words.
|
||||||
|
* @param {string} s The input string
|
||||||
|
* @returns {String[]} An array of words
|
||||||
|
*/
|
||||||
|
function getWords(s){
|
||||||
|
s = s.replace(/(^\s*)|(\s*$)/gi, '');
|
||||||
|
s = s.replace(/[ ]{2,}/gi, ' ');
|
||||||
|
s = s.replace(/\n/g, ' ');
|
||||||
|
return s.split(' ').filter(String);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns the number of words in a string.
|
||||||
|
* @param {string} s The input string
|
||||||
|
* @returns {number} The number of words
|
||||||
|
*/
|
||||||
|
function countWords(s){
|
||||||
|
return getWords(s).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause execution for a desired amount of time. Use this in `async` functions with `await sleep(ms)`.
|
||||||
|
* @param {Number} ms The number of milliseconds to sleep for
|
||||||
|
* @returns {Promise<undefined>}
|
||||||
|
*/
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuffles an array using `Math.random()` to swap elements and returns the result.
|
||||||
|
* @param {array} arr The input array
|
||||||
|
* @returns {array} The shuffled array
|
||||||
|
*/
|
||||||
|
function shuffle(arr) {
|
||||||
|
let i = 0;
|
||||||
|
while (i < arr.length) {
|
||||||
|
let tmp = arr[i];
|
||||||
|
let num = Math.round(Math.random()*(arr.length-1));
|
||||||
|
arr[i] = arr[num];
|
||||||
|
arr[num] = tmp;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a number of seconds into `[hh]:[m]m:ss` format.
|
||||||
|
* @param {number} s The number of seconds to format
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
function formatSeconds(s) {
|
||||||
|
s = Math.floor(s || 0);
|
||||||
|
let hours = 0;
|
||||||
|
let minutes = 0;
|
||||||
|
let seconds = s;
|
||||||
|
if (s >= 3600) {
|
||||||
|
hours = Math.floor(s / 3600);
|
||||||
|
s = s % 3600;
|
||||||
|
}
|
||||||
|
if (s >= 60) {
|
||||||
|
minutes = Math.floor(s / 60);
|
||||||
|
s = s % 60;
|
||||||
|
}
|
||||||
|
seconds = s;
|
||||||
|
let timeString = '';
|
||||||
|
timeString += (hours > 0) ? hours.toString() + ':' : '';
|
||||||
|
timeString += minutes.toString().padStart((hours > 0) ? 2 : 1, '0') + ':';
|
||||||
|
timeString += seconds.toString().padStart(2, '0');
|
||||||
|
return timeString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get age as an integer from a Date object.
|
||||||
|
* @param {Date} date The target date
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function getAgeFromDate(date) {
|
||||||
|
const now = new Date();
|
||||||
|
let age = now.getFullYear()-date.getFullYear();
|
||||||
|
if (now.getMonth() < date.getMonth() || (now.getMonth() == date.getMonth() && now.getDate() < date.getDate())) {
|
||||||
|
age--;
|
||||||
|
}
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string describing the relative time between two dates, like "3 days ago" or "2 months from now".
|
||||||
|
* @param {number} target A millisecond-timestamp of the target date.
|
||||||
|
* @param {number} [anchor] A millisecond-timestamp of the date to look from. Defaults to `Date.now()`.
|
||||||
|
* @returns {string} The resulting relative description.
|
||||||
|
*/
|
||||||
|
function getRelativeDate(target, anchor = Date.now()) {
|
||||||
|
const isFuture = (anchor-target < 0) ? true : false;
|
||||||
|
let diff = Math.abs(anchor-target);
|
||||||
|
diff = Math.round(diff/1000);
|
||||||
|
if (diff < 120) // Less than 120 seconds
|
||||||
|
return (isFuture) ? `In a moment` : `Moments ago`;
|
||||||
|
diff = Math.round(diff/60);
|
||||||
|
if (diff < 120) // Less than 120 minutes
|
||||||
|
return (isFuture) ? `${diff} mins from now` : `${diff} mins ago`;
|
||||||
|
diff = Math.round(diff/60);
|
||||||
|
if (diff < 72) // Less than 72 hours
|
||||||
|
return (isFuture) ? `${diff} hours from now` : `${diff} hours ago`;
|
||||||
|
diff = Math.round(diff/24);
|
||||||
|
const days = diff;
|
||||||
|
if (diff < 90) // Less than 90 days
|
||||||
|
return (isFuture) ? `${diff} days from now` : `${diff} days ago`;
|
||||||
|
diff = Math.round(diff/30.43685);
|
||||||
|
if (diff < 36) // Less than 36 months
|
||||||
|
return (isFuture) ? `${diff} months from now` : `${diff} months ago`;
|
||||||
|
diff = Math.round(days/365.2422);
|
||||||
|
return (isFuture) ? `${diff} years from now` : `${diff} years ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a number of bytes to a human-readable size, like "230.2 MB" or "7.27 GB".
|
||||||
|
* @param {number} bytes A number of bytes to convert
|
||||||
|
* @returns {string} The human-readable size string
|
||||||
|
*/
|
||||||
|
function formatSize(bytes) {
|
||||||
|
if (bytes < 1000) return `${bytes} B`;
|
||||||
|
bytes /= 1024;
|
||||||
|
if (bytes < (1000)) return `${roundSmart(bytes, 0)} KB`;
|
||||||
|
bytes /= 1024;
|
||||||
|
if (bytes < (1000)) return `${roundSmart(bytes, 1)} MB`;
|
||||||
|
bytes /= 1024;
|
||||||
|
if (bytes < (1000)) return `${roundSmart(bytes, 2)} GB`;
|
||||||
|
bytes /= 1024;
|
||||||
|
if (bytes < (1000)) return `${roundSmart(bytes, 2)} TB`;
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines of an input string is a valid URL.
|
||||||
|
* @param {string} string The input string
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
function isValidUrl(string) {
|
||||||
|
let url;
|
||||||
|
try {
|
||||||
|
url = new URL(string);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return url.protocol === "http:" || url.protocol === "https:";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a string is a valid hostname.
|
||||||
|
* @param {string} string The input string
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
function isValidHostname(string) {
|
||||||
|
return string.match(/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/) && !string.match(/^localhost$/);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a string is a valid IPv4 or IPv6 address.
|
||||||
|
* @param {string} string The input string
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
function isValidIp(string) {
|
||||||
|
return string.match(/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/) && !string.match(/(^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)/) && !string.match(/^::1$/);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes HTML entities present in the input string and returns the result.
|
||||||
|
* @param {string} text The input string
|
||||||
|
* @returns {string} The escaped string
|
||||||
|
*/
|
||||||
|
function escapeHTML(text) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
module.exports = {
|
||||||
|
randomHex: randomHex,
|
||||||
|
randomInt: randomInt,
|
||||||
|
randomFloat: randomFloat,
|
||||||
|
getRandomElement: getRandomElement,
|
||||||
|
getRandomWeighted: getRandomWeighted,
|
||||||
|
clamp: clamp,
|
||||||
|
overflow: overflow,
|
||||||
|
roundSmart: roundSmart,
|
||||||
|
getWords: getWords,
|
||||||
|
countWords: countWords,
|
||||||
|
sleep: sleep,
|
||||||
|
shuffle: shuffle,
|
||||||
|
formatSeconds: formatSeconds,
|
||||||
|
getAgeFromDate: getAgeFromDate,
|
||||||
|
getRelativeDate: getRelativeDate,
|
||||||
|
formatSize: formatSize,
|
||||||
|
isValidUrl: isValidUrl,
|
||||||
|
isValidHostname: isValidHostname,
|
||||||
|
isValidIp: isValidIp,
|
||||||
|
escapeHTML: escapeHTML
|
||||||
|
};
|
||||||
|
} catch (error) {}
|
1178
web/v2/base.css
Normal file
1178
web/v2/base.css
Normal file
File diff suppressed because it is too large
Load Diff
810
web/v2/base.js
Normal file
810
web/v2/base.js
Normal file
@ -0,0 +1,810 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* A shortcut for `*.querySelector()`.
|
||||||
|
* @param {string} id The target query selector
|
||||||
|
* @param {HTMLElement} ancestor The ancestor element to start from
|
||||||
|
* @returns {HTMLElement|undefined} The selected element
|
||||||
|
*/
|
||||||
|
function $(selector, ancestor = document) {
|
||||||
|
return ancestor.querySelector(selector);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* A shortcut for `*.querySelectorAll()`.
|
||||||
|
* @param {string} id The target query selector
|
||||||
|
* @param {HTMLElement} ancestor The ancestor element to start from
|
||||||
|
* @returns {NodeListOf<any>} The selected elements
|
||||||
|
*/
|
||||||
|
function $$(selector, ancestor = document) {
|
||||||
|
return ancestor.querySelectorAll(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positions an element at the given coordinates, ensuring it stays within the window.
|
||||||
|
* @param {HTMLElement} element The element
|
||||||
|
* @param {number} x Left
|
||||||
|
* @param {number} y Top
|
||||||
|
*/
|
||||||
|
function positionElement(element, x, y) {
|
||||||
|
const elementWidth = element.offsetWidth;
|
||||||
|
const elementHeight = element.offsetHeight;
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
if (x + elementWidth > windowWidth) {
|
||||||
|
x = x - elementWidth;
|
||||||
|
if (x < 0) x = 0;
|
||||||
|
}
|
||||||
|
if (y + elementHeight > windowHeight) {
|
||||||
|
y = y - elementHeight;
|
||||||
|
if (y < 0) y = 0;
|
||||||
|
}
|
||||||
|
element.style.left = x + 'px';
|
||||||
|
element.style.top = y + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isElementVisible(el) {
|
||||||
|
if (!el || typeof el.getBoundingClientRect !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return !!(rect.width || rect.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a popup
|
||||||
|
*/
|
||||||
|
class PopupBuilder {
|
||||||
|
#shouldEscapeClose = true;
|
||||||
|
#elCont; #elTitle; #elBody; #elActions;
|
||||||
|
#onShow = () => {};
|
||||||
|
#onHide = () => {};
|
||||||
|
constructor() {
|
||||||
|
// Create container
|
||||||
|
this.#elCont = document.createElement('div');
|
||||||
|
this.#elCont.classList.add('popupCont');
|
||||||
|
// Create popup element
|
||||||
|
this.el = document.createElement('div');
|
||||||
|
this.el.classList.add('popup');
|
||||||
|
this.#elCont.appendChild(this.el);
|
||||||
|
// Add title
|
||||||
|
this.#elTitle = document.createElement('div');
|
||||||
|
this.#elTitle.classList.add('title', 'flex-no-shrink');
|
||||||
|
this.#elTitle.innerText = 'Popup';
|
||||||
|
this.el.appendChild(this.#elTitle);
|
||||||
|
// Add body
|
||||||
|
this.#elBody = document.createElement('div');
|
||||||
|
this.#elBody.classList.add('body');
|
||||||
|
this.el.appendChild(this.#elBody);
|
||||||
|
// Add actions container
|
||||||
|
this.#elActions = document.createElement('div');
|
||||||
|
this.#elActions.classList.add('actions', 'flex-no-shrink');
|
||||||
|
this.el.appendChild(this.#elActions);
|
||||||
|
// Set up listeners
|
||||||
|
this.#elCont.addEventListener('click', () => {
|
||||||
|
if (this.#shouldEscapeClose) this.hide();
|
||||||
|
});
|
||||||
|
this.el.addEventListener('click', e => e.stopPropagation());
|
||||||
|
// Set up focus trap
|
||||||
|
this.trap = null;
|
||||||
|
try {
|
||||||
|
this.trap = focusTrap.createFocusTrap(this.#elCont, {
|
||||||
|
initialFocus: false,
|
||||||
|
escapeDeactivates: false
|
||||||
|
});
|
||||||
|
} catch(error) {}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the title of the popup.
|
||||||
|
* @param {string} title The title of the popup
|
||||||
|
* @returns {PopupBuilder}
|
||||||
|
*/
|
||||||
|
setTitle(title) {
|
||||||
|
this.#elTitle.innerText = title;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds an HTML element to the body of the popup.
|
||||||
|
* @param {HTMLElement} element The element
|
||||||
|
* @returns {PopupBuilder}
|
||||||
|
*/
|
||||||
|
addBody(element) {
|
||||||
|
this.#elBody.appendChild(element);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Appends raw HTML to the body of the popup.
|
||||||
|
* @param {string} html An HTML string
|
||||||
|
* @returns {PopupBuilder}
|
||||||
|
*/
|
||||||
|
addBodyHTML(html) {
|
||||||
|
this.#elBody.insertAdjacentHTML('beforeend', html);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets if clicking outside the popup or pressing escape will close the popup. Defaults to `true`.
|
||||||
|
* @param {boolean} shouldClose
|
||||||
|
* @returns {PopupBuilder}
|
||||||
|
*/
|
||||||
|
setClickOutside(shouldClose) {
|
||||||
|
this.#shouldEscapeClose = shouldClose;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets a callback function to run when the popup is shown.
|
||||||
|
* @param {callback} onShow The callback to run
|
||||||
|
* @returns {PopupBuilder}
|
||||||
|
*/
|
||||||
|
setOnShow(onShow) {
|
||||||
|
this.#onShow = onShow;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets a callback function to run when the popup is hidden.
|
||||||
|
* @param {callback} onHide The callback to run
|
||||||
|
* @returns {PopupBuilder}
|
||||||
|
*/
|
||||||
|
setOnHide(onHide) {
|
||||||
|
this.#onHide = onHide;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Shows the popup.
|
||||||
|
*/
|
||||||
|
show() {
|
||||||
|
document.body.appendChild(this.#elCont);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.#elCont.classList.add('visible');
|
||||||
|
try {
|
||||||
|
this.trap.activate();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Unable to trap focus inside popup. Make sure focus-trap and tabbable are available.`);
|
||||||
|
}
|
||||||
|
this.#onShow();
|
||||||
|
}, 1);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Hides the popup and removes it from the DOM.
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.#elCont.classList.remove('visible');
|
||||||
|
try {
|
||||||
|
this.trap.deactivate();
|
||||||
|
} catch (error) {}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.#elCont.parentNode.removeChild(this.#elCont);
|
||||||
|
}, 200);
|
||||||
|
this.#onHide();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @callback PopupBuilderAddActionCallback
|
||||||
|
* @param {PopupActionBuilder} actionBuilder An action builder to be modified and returned.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Adds an action button to the bottom of the popup. These buttons are displayed from right to left.
|
||||||
|
* @param {PopupBuilderAddActionCallback} callback A callback function that returns a `PopupActionBuilder` object. This callback is passed a new `PopupActionBuilder` object that can be modified and returned.
|
||||||
|
*/
|
||||||
|
addAction(callback) {
|
||||||
|
const builder = new PopupActionBuilder(this);
|
||||||
|
const action = callback(builder);
|
||||||
|
this.#elActions.appendChild(action.el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Builds a popup action button
|
||||||
|
*/
|
||||||
|
class PopupActionBuilder {
|
||||||
|
#shouldClose = true;
|
||||||
|
#onClick = () => {};
|
||||||
|
/**
|
||||||
|
* @param {PopupBuilder} parent The parent popup builder.
|
||||||
|
*/
|
||||||
|
constructor(parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.el = document.createElement('button');
|
||||||
|
this.el.classList.add('btn', 'secondary');
|
||||||
|
this.el.innerText = `Button`;
|
||||||
|
this.el.addEventListener('click', () => {
|
||||||
|
if (this.el.disabled) return;
|
||||||
|
this.#onClick();
|
||||||
|
if (this.#shouldClose) parent.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets whether this action button should use the primary button style. Defaults to `false`.
|
||||||
|
* @param {boolean} isPrimary
|
||||||
|
* @returns {PopupActionBuilder}
|
||||||
|
*/
|
||||||
|
setIsPrimary(isPrimary) {
|
||||||
|
if (isPrimary) this.el.classList.remove('secondary');
|
||||||
|
else this.el.classList.add('secondary');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets whether this action button should use the danger button style. Defaults to `false`.
|
||||||
|
* @param {boolean} isDanger
|
||||||
|
* @returns {PopupActionBuilder}
|
||||||
|
*/
|
||||||
|
setIsDanger(isDanger) {
|
||||||
|
if (isDanger) this.el.classList.add('danger');
|
||||||
|
else this.el.classList.remove('danger');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets whether clicking this action should close the popup. Defaults to `true`.
|
||||||
|
* @param {boolean} shouldClose
|
||||||
|
* @returns {PopupActionBuilder}
|
||||||
|
*/
|
||||||
|
setShouldClose(shouldClose) {
|
||||||
|
this.#shouldClose = shouldClose;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the label of this action button.
|
||||||
|
* @param {string} label The button's label
|
||||||
|
* @returns {PopupActionBuilder}
|
||||||
|
*/
|
||||||
|
setLabel(label) {
|
||||||
|
this.el.innerText = label;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Disables the action button.
|
||||||
|
* @returns {PopupActionBuilder}
|
||||||
|
*/
|
||||||
|
disable() {
|
||||||
|
this.el.disabled = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Enables the action button.
|
||||||
|
* @returns {PopupActionBuilder}
|
||||||
|
*/
|
||||||
|
enable() {
|
||||||
|
this.el.disabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets a callback to be run when this action is clicked.
|
||||||
|
* @param {callback} onClick The callback
|
||||||
|
* @returns {PopupActionBuilder}
|
||||||
|
*/
|
||||||
|
setClickHandler(onClick) {
|
||||||
|
this.#onClick = onClick;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a context menu
|
||||||
|
*/
|
||||||
|
class ContextMenuBuilder {
|
||||||
|
#elCont;
|
||||||
|
constructor() {
|
||||||
|
// Create container
|
||||||
|
this.#elCont = document.createElement('div');
|
||||||
|
this.#elCont.classList.add('contextCont');
|
||||||
|
// Create popup element
|
||||||
|
this.el = document.createElement('div');
|
||||||
|
this.el.classList.add('context');
|
||||||
|
this.#elCont.appendChild(this.el);
|
||||||
|
// Hide when clicking outside
|
||||||
|
this.#elCont.addEventListener('click', () => {
|
||||||
|
this.hide();
|
||||||
|
});
|
||||||
|
this.el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
// Create focus trap
|
||||||
|
try {
|
||||||
|
this.trap = focusTrap.createFocusTrap(this.#elCont, {
|
||||||
|
initialFocus: false,
|
||||||
|
escapeDeactivates: false
|
||||||
|
});
|
||||||
|
} catch(error) {}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @callback ContextMenuBuilderAddItemCallback
|
||||||
|
* @param {ContextMenuItemBuilder} itemBuilder An item builder to be modified and returned.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Adds a clickable item to the context menu.
|
||||||
|
* @param {ContextMenuBuilderAddItemCallback} callback A callback function that returns a `ContextMenuItemBuilder` object. This callback is passed a new `ContextMenuItemBuilder` object that can be modified and returned.
|
||||||
|
* @returns {ContextMenuBuilder}
|
||||||
|
*/
|
||||||
|
addItem(callback) {
|
||||||
|
const builder = new ContextMenuItemBuilder(this);
|
||||||
|
const item = callback(builder);
|
||||||
|
this.el.appendChild(item.el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Adds a separator to the context menu.
|
||||||
|
* @returns {ContextMenuBuilder}
|
||||||
|
*/
|
||||||
|
addSeparator() {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.classList.add('separator');
|
||||||
|
this.el.appendChild(el);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets if menu item icons should be shown. If `true`, the space an icon takes up is still visible even on items without set icons. If `false`, all item icons are hidden, regardless of whether they're set or not. Defaults to `true`.
|
||||||
|
* @param {boolean} areVisible The new state
|
||||||
|
* @returns {ContextMenuBuilder}
|
||||||
|
*/
|
||||||
|
setIconVisibility(areVisible) {
|
||||||
|
this.el.classList.toggle('hideIcons', !areVisible);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Shows the context menu at the specified fixed coordinates. This method is used by the other show methods.
|
||||||
|
* @param {number} x Left
|
||||||
|
* @param {number} y Top
|
||||||
|
*/
|
||||||
|
showAtCoords(x, y) {
|
||||||
|
document.body.appendChild(this.#elCont);
|
||||||
|
this.#elCont.classList.remove('ani');
|
||||||
|
this.#elCont.classList.remove('visible');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.el.style.scale = '1';
|
||||||
|
positionElement(this.el, x, y);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.#elCont.classList.add('ani');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.#elCont.classList.add('visible');
|
||||||
|
try {
|
||||||
|
this.trap.activate();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Unable to trap focus inside context menu. Make sure focus-trap and tabbable are available.`);
|
||||||
|
}
|
||||||
|
}, 1);
|
||||||
|
}, 10);
|
||||||
|
}, 1);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Shows the context menu originating from the center of an HTML element.
|
||||||
|
* @param {HTMLElement} el The target element
|
||||||
|
* @returns {ContextMenuBuilder}
|
||||||
|
*/
|
||||||
|
showAtElement(el) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return this.showAtCoords((rect.left+(rect.width/2)), (rect.top+(rect.height/2)));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Shows the context menu at the current mouse position, or fixed in the top left if there hasn't been any mouse movement.
|
||||||
|
* @returns {ContextMenuBuilder}
|
||||||
|
*/
|
||||||
|
showAtCursor() {
|
||||||
|
return this.showAtCoords(mouse.x+5, mouse.y);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Hides the context menu and removes it from the DOM.
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.#elCont.classList.remove('visible');
|
||||||
|
try {
|
||||||
|
this.trap.deactivate();
|
||||||
|
} catch (error) {}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.#elCont.parentNode.removeChild(this.#elCont);
|
||||||
|
}, 200);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Builds a clickable context menu item
|
||||||
|
*/
|
||||||
|
class ContextMenuItemBuilder {
|
||||||
|
#onClick = () => {};
|
||||||
|
constructor(parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
// Create item container
|
||||||
|
this.el = document.createElement('button');
|
||||||
|
this.el.classList.add('item');
|
||||||
|
// Create icon
|
||||||
|
this.elIcon = document.createElement('div');
|
||||||
|
this.elIcon.classList.add('icon', 'hidden');
|
||||||
|
this.el.appendChild(this.elIcon);
|
||||||
|
// Create label
|
||||||
|
this.elLabel = document.createElement('div');
|
||||||
|
this.elLabel.classList.add('label');
|
||||||
|
this.elLabel.innerText = `Item`;
|
||||||
|
this.el.appendChild(this.elLabel);
|
||||||
|
// Set up click handler
|
||||||
|
this.el.addEventListener('click', () => {
|
||||||
|
if (this.el.disabled) return;
|
||||||
|
this.#onClick();
|
||||||
|
parent.hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the label for this menu item.
|
||||||
|
* @param {string} label The item's label
|
||||||
|
* @returns {ContextMenuItemBuilder}
|
||||||
|
*/
|
||||||
|
setLabel(label) {
|
||||||
|
this.elLabel.innerText = label;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the icon for this item. If unset, no icon will be shown.
|
||||||
|
* @param {string} icon A valid [Material Symbol](https://fonts.google.com/icons) string
|
||||||
|
*
|
||||||
|
* To make sure you're getting the right symbol, click on the icon, go to the **Android** tab, and copy the string in the code block.
|
||||||
|
* @returns {ContextMenuItemBuilder}
|
||||||
|
*/
|
||||||
|
setIcon(icon) {
|
||||||
|
this.elIcon.classList.remove('hidden');
|
||||||
|
this.elIcon.innerText = icon;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets hover tooltip text for this item.
|
||||||
|
* @param {string} text The tooltip text
|
||||||
|
* @returns {ContextMenuItemBuilder}
|
||||||
|
*/
|
||||||
|
setTooltip(text) {
|
||||||
|
this.el.title = text;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets whether or not the icon and text of this item should be red.
|
||||||
|
* @param {boolean} isDanger
|
||||||
|
* @returns {ContextMenuItemBuilder}
|
||||||
|
*/
|
||||||
|
setIsDanger(isDanger) {
|
||||||
|
this.elIcon.classList.toggle('text-danger', isDanger);
|
||||||
|
this.elLabel.classList.toggle('text-danger', isDanger);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Disables this menu item.
|
||||||
|
* @returns {ContextMenuItemBuilder}
|
||||||
|
*/
|
||||||
|
disable() {
|
||||||
|
this.el.disabled = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Enables this menu item.
|
||||||
|
* @returns {ContextMenuItemBuilder}
|
||||||
|
*/
|
||||||
|
enable() {
|
||||||
|
this.el.disabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets a callback to be run when this item is clicked.
|
||||||
|
* @param {callback} onClick The callback
|
||||||
|
* @returns {ContextMenuItemBuilder}
|
||||||
|
*/
|
||||||
|
setClickHandler(onClick) {
|
||||||
|
this.#onClick = onClick;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a toast notification overlay container.
|
||||||
|
*/
|
||||||
|
class ToastOverlay {
|
||||||
|
/**
|
||||||
|
* @param {('left'|'center'|'right')} hAlign Horizontal alignment
|
||||||
|
* @param {('top'|'bottom')} vAlign Vertical alignment
|
||||||
|
*/
|
||||||
|
constructor(hAlign = 'left', vAlign = 'bottom') {
|
||||||
|
this.el = document.createElement('div');
|
||||||
|
this.el.classList.add('toastOverlay', 'col', 'gap-10');
|
||||||
|
if (hAlign == 'left') this.el.classList.add('align-start');
|
||||||
|
if (hAlign == 'center') this.el.classList.add('align-center');
|
||||||
|
if (hAlign == 'right') this.el.classList.add('align-end');
|
||||||
|
if (vAlign == 'top') this.el.classList.add('justify-start');
|
||||||
|
if (vAlign == 'bottom') {
|
||||||
|
this.el.classList.add('justify-start');
|
||||||
|
this.el.style.flexDirection = 'column-reverse';
|
||||||
|
}
|
||||||
|
document.body.appendChild(this.el);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @callback ToastOverlayShowCallback
|
||||||
|
* @param {ToastBuilder} itemBuilder An item builder to be modified and returned.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Shows a new toast notification.
|
||||||
|
* @param {ToastOverlayShowCallback} callback A callback function that returns a `ToastBuilder` object. This callback is passed a new `ToastBuilder` object that can be modified and returned.
|
||||||
|
* @returns {ToastOverlay}
|
||||||
|
*/
|
||||||
|
showToast(callback) {
|
||||||
|
const builder = new ToastBuilder(this);
|
||||||
|
const toast = callback(builder);
|
||||||
|
this.el.insertAdjacentElement('afterbegin', toast.el);
|
||||||
|
setTimeout(() => {
|
||||||
|
const delay = toast.el.dataset.delay;
|
||||||
|
if (delay) setTimeout(() => {
|
||||||
|
toast.close();
|
||||||
|
}, delay);
|
||||||
|
toast.el.classList.add('visible');
|
||||||
|
if (delay) setTimeout(close, delay);
|
||||||
|
}, 100);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Builds a toast notification. This class is to be used alongside the `ToastOverlay` class.
|
||||||
|
*/
|
||||||
|
class ToastBuilder {
|
||||||
|
constructor() {
|
||||||
|
this.el = document.createElement('div');
|
||||||
|
this.el.classList.add('toast', 'row', 'gap-10', 'align-center');
|
||||||
|
this.elIcon = document.createElement('div');
|
||||||
|
this.elIcon.classList.add('icon', 'hidden');
|
||||||
|
this.el.appendChild(this.elIcon);
|
||||||
|
this.elBody = document.createElement('div');
|
||||||
|
this.elBody.classList.add('body');
|
||||||
|
this.elBody.innerText = 'Toast notification';
|
||||||
|
this.el.appendChild(this.elBody);
|
||||||
|
this.elClose = document.createElement('button');
|
||||||
|
this.elClose.classList.add('btn', 'secondary', 'iconOnly', 'small', 'close');
|
||||||
|
this.elClose.innerHTML = '<div class="icon">close</div>';
|
||||||
|
this.elClose.title = 'Dismiss';
|
||||||
|
this.elClose.addEventListener('click', () => this.close());
|
||||||
|
this.el.appendChild(this.elClose);
|
||||||
|
this.el.dataset.delay = 5000;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the icon to show on the left of the toast.
|
||||||
|
* @param {string} icon A valid [Material Symbol](https://fonts.google.com/icons) string
|
||||||
|
* @returns {ToastBuilder}
|
||||||
|
*/
|
||||||
|
setIcon(icon) {
|
||||||
|
this.elIcon.classList.remove('hidden');
|
||||||
|
this.elIcon.innerText = icon;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the body of the toast.
|
||||||
|
* @param {string} html The body HTML
|
||||||
|
* @returns {ToastBuilder}
|
||||||
|
*/
|
||||||
|
setBodyHTML(html) {
|
||||||
|
this.elBody.innerHTML = html;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the delay before the toast will automatically close after being shown. Set this to `0` to show the toast indefinitely (until the user closes it).
|
||||||
|
*
|
||||||
|
* Default is `5000` (5 seconds).
|
||||||
|
* @param {number} delayMs The delay in milliseconds.
|
||||||
|
* @returns {ToastBuilder}
|
||||||
|
*/
|
||||||
|
setCloseDelay(delayMs) {
|
||||||
|
this.el.dataset.delay = delayMs;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets whether or not the toast can be closed by the user. Rather, this determines if the close button is visible or not.
|
||||||
|
* @param {boolean} isCloseable
|
||||||
|
* @returns {ToastBuilder}
|
||||||
|
*/
|
||||||
|
setIsCloseable(isCloseable) {
|
||||||
|
if (isCloseable) {
|
||||||
|
this.elClose.style.display = '';
|
||||||
|
} else {
|
||||||
|
this.elClose.style.display = 'none';
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Closes the toast, assuming that it's visible.
|
||||||
|
* @returns {ToastBuilder}
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
this.el.classList.remove('visible');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.el.parentNode) return;
|
||||||
|
this.el.parentNode.removeChild(this.el);
|
||||||
|
}, 200);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptUrlDownload(url, name) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url
|
||||||
|
a.download = name || url.split('/').pop();
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and append the tooltip element
|
||||||
|
const tooltip = document.createElement('div');
|
||||||
|
tooltip.classList.add('tooltip');
|
||||||
|
tooltip.role = 'tooltip';
|
||||||
|
tooltip.id = Date.now();
|
||||||
|
document.body.appendChild(tooltip);
|
||||||
|
let activeTooltipElement = null;
|
||||||
|
|
||||||
|
// Functions for the tooltip
|
||||||
|
let tooltipTimeout;
|
||||||
|
const showTooltip = el => {
|
||||||
|
hideTooltip();
|
||||||
|
tooltipTimeout = setTimeout(() => {
|
||||||
|
// After 200ms, remove transitions and reset scale
|
||||||
|
tooltip.style.transition = 'none';
|
||||||
|
// Set the tooltip's content
|
||||||
|
tooltip.innerHTML = el.dataset.tooltip;
|
||||||
|
setTimeout(() => {
|
||||||
|
// Position the tooltip and add transitions
|
||||||
|
positionElement(tooltip, mouse.x+5, mouse.y);
|
||||||
|
tooltip.style.transition = '';
|
||||||
|
tooltipTimeout = setTimeout(() => {
|
||||||
|
// Show the tooltip if the mouse is still over the element
|
||||||
|
const isMouseOver = el.dataset.isMouseOver === 'true';
|
||||||
|
if (!isMouseOver || !isElementVisible(el)) return;
|
||||||
|
tooltip.classList.add('visible');
|
||||||
|
}, 200);
|
||||||
|
}, 50);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideTooltip = () => {
|
||||||
|
clearTimeout(tooltipTimeout);
|
||||||
|
tooltip.classList.remove('visible');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Continuously save mouse position
|
||||||
|
const mouse = { x: 0, y: 0 };
|
||||||
|
window.addEventListener('mousemove', (e) => {
|
||||||
|
mouse.x = e.clientX;
|
||||||
|
mouse.y = e.clientY;
|
||||||
|
// Show or hide the tooltip accordingly
|
||||||
|
if (tooltip.classList.contains('visible')) {
|
||||||
|
hideTooltip();
|
||||||
|
}
|
||||||
|
if (activeTooltipElement) {
|
||||||
|
showTooltip(activeTooltipElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle dynamic changes
|
||||||
|
document.addEventListener('domChange', () => {
|
||||||
|
|
||||||
|
// Get elements with a title attribute
|
||||||
|
const titleEls = $$('[title]:not([data-no-tooltip])');
|
||||||
|
for (const el of titleEls) {
|
||||||
|
// Remove the title attribute and add it to the tooltip data attribute
|
||||||
|
const title = el.title;
|
||||||
|
el.removeAttribute('title');
|
||||||
|
el.dataset.tooltip = title;
|
||||||
|
// Add aria label
|
||||||
|
el.setAttribute('aria-describedby', tooltip.id);
|
||||||
|
}
|
||||||
|
// Get elements with a tooltip data attribute
|
||||||
|
const tooltipEls = $$('[data-tooltip]:not([data-has-tooltip])');
|
||||||
|
// Loop through 'em
|
||||||
|
for (const el of tooltipEls) {
|
||||||
|
el.dataset.isMouseOver = false;
|
||||||
|
const show = () => {
|
||||||
|
el.dataset.isMouseOver = true;
|
||||||
|
activeTooltipElement = el;
|
||||||
|
};
|
||||||
|
const hide = () => {
|
||||||
|
el.dataset.isMouseOver = false;
|
||||||
|
activeTooltipElement = null;
|
||||||
|
};
|
||||||
|
el.addEventListener('mousemove', show);
|
||||||
|
el.addEventListener('mouseleave', hide);
|
||||||
|
el.addEventListener('mousedown', hide);
|
||||||
|
el.addEventListener('mouseup', hide);
|
||||||
|
// Mark the tooltip as added
|
||||||
|
el.dataset.hasTooltip = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get slider elements
|
||||||
|
const sliders = $$('div.slider:not([data-modified])');
|
||||||
|
// Loop through 'em
|
||||||
|
for (const slider of sliders) {
|
||||||
|
// Create elements
|
||||||
|
const prog = document.createElement('progress');
|
||||||
|
const input = document.createElement('input');
|
||||||
|
// Set attributes
|
||||||
|
let textbox;
|
||||||
|
const onSliderChange = () => {
|
||||||
|
// Collect data values
|
||||||
|
const min = slider.dataset.min || 0;
|
||||||
|
const max = slider.dataset.max || 100;
|
||||||
|
const step = slider.dataset.step || 1;
|
||||||
|
const value = slider.dataset.value || 0;
|
||||||
|
const rangeId = slider.dataset.rangeId;
|
||||||
|
const progId = slider.dataset.progId;
|
||||||
|
textbox = $(slider.dataset.textbox);
|
||||||
|
// Set attributes
|
||||||
|
input.type = 'range';
|
||||||
|
input.min = min;
|
||||||
|
input.max = max;
|
||||||
|
input.value = value;
|
||||||
|
input.step = step;
|
||||||
|
prog.min = min;
|
||||||
|
prog.max = max;
|
||||||
|
prog.value = value;
|
||||||
|
prog.step = step;
|
||||||
|
if (progId) prog.id = progId || '';
|
||||||
|
if (rangeId) input.id = rangeId || '';
|
||||||
|
// Handle the textbox
|
||||||
|
if (textbox) {
|
||||||
|
textbox.type = 'number';
|
||||||
|
textbox.min = min;
|
||||||
|
textbox.max = max;
|
||||||
|
textbox.step = step;
|
||||||
|
textbox.value = value;
|
||||||
|
textbox.oninput = () => {
|
||||||
|
input.value = textbox.value;
|
||||||
|
input.dispatchEvent(new Event('input'));
|
||||||
|
};
|
||||||
|
textbox.onchange = textbox.oninput;
|
||||||
|
}
|
||||||
|
// Dispatch events
|
||||||
|
input.dispatchEvent(new Event('change'));
|
||||||
|
prog.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
onSliderChange();
|
||||||
|
// Append elements
|
||||||
|
slider.appendChild(prog);
|
||||||
|
slider.appendChild(input);
|
||||||
|
// Add event listeners
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
slider.dataset.value = input.value;
|
||||||
|
prog.value = slider.dataset.value;
|
||||||
|
if (textbox) textbox.value = slider.dataset.value;
|
||||||
|
slider.dispatchEvent(new Event('input'));
|
||||||
|
});
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
slider.dataset.value = input.value;
|
||||||
|
prog.value = slider.dataset.value;
|
||||||
|
if (textbox) textbox.value = slider.dataset.value;
|
||||||
|
});
|
||||||
|
prog.addEventListener('change', () => {
|
||||||
|
input.value = slider.dataset.value;
|
||||||
|
if (textbox) textbox.value = slider.dataset.value;
|
||||||
|
});
|
||||||
|
slider.addEventListener('change', onSliderChange);
|
||||||
|
// Mark the slider as added
|
||||||
|
slider.dataset.modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get expandable textareas
|
||||||
|
const textareas = $$('textarea[data-make-expandable]:not([data-modified])');
|
||||||
|
// Loop through 'em
|
||||||
|
for (const textarea of textareas) {
|
||||||
|
textarea.addEventListener('resize', () => {
|
||||||
|
if (!isElementVisible(textarea) || !textarea.scrollHeight) return;
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px';
|
||||||
|
});
|
||||||
|
textarea.dispatchEvent(new Event('resize'));
|
||||||
|
textarea.addEventListener('input', () => {
|
||||||
|
textarea.dispatchEvent(new Event('resize'));
|
||||||
|
});
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
textarea.dispatchEvent(new Event('resize'));
|
||||||
|
});
|
||||||
|
textarea.dataset.modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle DOM mutations and dispatching the domChange event
|
||||||
|
const mutationObs = new MutationObserver(() => {
|
||||||
|
document.dispatchEvent(new Event('domChange'));
|
||||||
|
});
|
||||||
|
mutationObs.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
characterData: true,
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
window.addEventListener('domcontentloaded', () => {
|
||||||
|
document.dispatchEvent(new Event('domChange'));
|
||||||
|
});
|
338
web/v2/themes.css
Normal file
338
web/v2/themes.css
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
See `themes.json` for some metadata about each theme.
|
||||||
|
- The `themeColor` property is the suggested value for the `theme-color` meta tag.
|
||||||
|
- Equal to the theme's `--b3` color.
|
||||||
|
- The `name` property is the theme's canonical name.
|
||||||
|
- The `hue` property is the suggested hue to use for colouring custom elements
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
.lightblue,
|
||||||
|
.lightyellow,
|
||||||
|
.lightgreen,
|
||||||
|
.lightmuted,
|
||||||
|
.lightpurple {
|
||||||
|
/* Background and foreground */
|
||||||
|
--b0: hsl(180, 60%, 95%);
|
||||||
|
--b1: hsl(180, 60%, 90%);
|
||||||
|
--b2: hsl(180, 50%, 85%);
|
||||||
|
--b3: hsl(180, 40%, 80%);
|
||||||
|
--b4: hsl(180, 40%, 70%);
|
||||||
|
--b5: hsl(180, 40%, 60%);
|
||||||
|
--f4: hsl(180, 30%, 50%);
|
||||||
|
--f3: hsl(180, 30%, 35%);
|
||||||
|
--f2: hsl(180, 30%, 15%);
|
||||||
|
--f1: black;
|
||||||
|
/* Green accent */
|
||||||
|
--green0: hsl(120, 60%, 40%);
|
||||||
|
--green1: hsl(120, 60%, 65%);
|
||||||
|
--green2: hsl(120, 60%, 80%);
|
||||||
|
--green3: hsl(120, 60%, 85%);
|
||||||
|
--green4: hsl(120, 60%, 90%);
|
||||||
|
--green5: hsl(120, 60%, 95%);
|
||||||
|
/* Yellow accent */
|
||||||
|
--yellow0: hsl(40, 100%, 40%);
|
||||||
|
--yellow1: hsl(40, 100%, 70%);
|
||||||
|
--yellow2: hsl(40, 100%, 80%);
|
||||||
|
--yellow3: hsl(40, 100%, 85%);
|
||||||
|
--yellow4: hsl(40, 100%, 90%);
|
||||||
|
--yellow5: hsl(40, 100%, 95%);
|
||||||
|
/* Blue accent */
|
||||||
|
--blue0: hsl(210, 80%, 45%);
|
||||||
|
--blue1: hsl(210, 80%, 70%);
|
||||||
|
--blue2: hsl(210, 80%, 80%);
|
||||||
|
--blue3: hsl(210, 80%, 85%);
|
||||||
|
--blue4: hsl(210, 80%, 90%);
|
||||||
|
--blue5: hsl(210, 80%, 95%);
|
||||||
|
/* Danger */
|
||||||
|
--red0: hsl(355, 80%, 55%);
|
||||||
|
--red1: hsl(355, 80%, 75%);
|
||||||
|
--red2: hsl(355, 80%, 80%);
|
||||||
|
--red3: hsl(355, 80%, 85%);
|
||||||
|
--red4: hsl(355, 80%, 90%);
|
||||||
|
--red5: hsl(355, 80%, 95%);
|
||||||
|
/* Code */
|
||||||
|
--syntax-comment: #6e7781;
|
||||||
|
--syntax-constant: #0550ae;
|
||||||
|
--syntax-entity: #8250df;
|
||||||
|
--syntax-storage-modifier-import: #24292f;
|
||||||
|
--syntax-entity-tag: #116329;
|
||||||
|
--syntax-keyword: #cf222e;
|
||||||
|
--syntax-string: #0a3069;
|
||||||
|
--syntax-variable: #953800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightpurple {
|
||||||
|
/* Background and foreground */
|
||||||
|
--b0: hsl(280, 60%, 95%);
|
||||||
|
--b1: hsl(280, 60%, 90%);
|
||||||
|
--b2: hsl(280, 60%, 85%);
|
||||||
|
--b3: hsl(280, 50%, 80%);
|
||||||
|
--b4: hsl(290, 50%, 70%);
|
||||||
|
--b5: hsl(300, 40%, 60%);
|
||||||
|
--f4: hsl(310, 30%, 50%);
|
||||||
|
--f3: hsl(320, 30%, 35%);
|
||||||
|
--f2: hsl(320, 30%, 15%);
|
||||||
|
--f1: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightyellow {
|
||||||
|
/* Background and foreground */
|
||||||
|
--b0: hsl(50, 60%, 95%);
|
||||||
|
--b1: hsl(50, 60%, 90%);
|
||||||
|
--b2: hsl(40, 60%, 85%);
|
||||||
|
--b3: hsl(30, 50%, 80%);
|
||||||
|
--b4: hsl(30, 50%, 70%);
|
||||||
|
--b5: hsl(20, 40%, 60%);
|
||||||
|
--f4: hsl(10, 30%, 50%);
|
||||||
|
--f3: hsl(10, 30%, 35%);
|
||||||
|
--f2: hsl(0, 30%, 15%);
|
||||||
|
--f1: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightgreen {
|
||||||
|
/* Background and foreground */
|
||||||
|
--b0: hsl(100, 60%, 95%);
|
||||||
|
--b1: hsl(100, 60%, 90%);
|
||||||
|
--b2: hsl(110, 60%, 85%);
|
||||||
|
--b3: hsl(120, 50%, 80%);
|
||||||
|
--b4: hsl(120, 50%, 70%);
|
||||||
|
--b5: hsl(130, 40%, 60%);
|
||||||
|
--f4: hsl(140, 30%, 50%);
|
||||||
|
--f3: hsl(140, 30%, 35%);
|
||||||
|
--f2: hsl(150, 30%, 15%);
|
||||||
|
--f1: black;
|
||||||
|
--green0: hsl(180, 60%, 50%);
|
||||||
|
--green1: hsl(180, 60%, 70%);
|
||||||
|
--green2: hsl(180, 60%, 80%);
|
||||||
|
--green3: hsl(180, 60%, 85%);
|
||||||
|
--green4: hsl(180, 60%, 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightmuted {
|
||||||
|
/* Background and foreground */
|
||||||
|
--b0: hsl(160, 10%, 95%);
|
||||||
|
--b1: hsl(170, 10%, 90%);
|
||||||
|
--b2: hsl(180, 10%, 85%);
|
||||||
|
--b3: hsl(190, 10%, 80%);
|
||||||
|
--b4: hsl(200, 10%, 70%);
|
||||||
|
--b5: hsl(210, 10%, 60%);
|
||||||
|
--f4: hsl(220, 10%, 50%);
|
||||||
|
--f3: hsl(230, 10%, 35%);
|
||||||
|
--f2: hsl(240, 10%, 15%);
|
||||||
|
--f1: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkpurple,
|
||||||
|
.darkyellow,
|
||||||
|
.darkgreen,
|
||||||
|
.darkmuted,
|
||||||
|
.darkblue {
|
||||||
|
/* Background and foreground */
|
||||||
|
--b0: hsl(270, 30%, 15%);
|
||||||
|
--b1: hsl(270, 30%, 20%);
|
||||||
|
--b2: hsl(270, 30%, 25%);
|
||||||
|
--b3: hsl(270, 30%, 30%);
|
||||||
|
--b4: hsl(280, 30%, 40%);
|
||||||
|
--b5: hsl(290, 30%, 50%);
|
||||||
|
--f4: hsl(300, 30%, 65%);
|
||||||
|
--f3: hsl(310, 40%, 80%);
|
||||||
|
--f2: hsl(310, 50%, 90%);
|
||||||
|
--f1: white;
|
||||||
|
/* Green accent */
|
||||||
|
--green0: hsl(150, 50%, 30%);
|
||||||
|
--green1: hsl(150, 50%, 40%);
|
||||||
|
--green2: hsl(150, 50%, 50%);
|
||||||
|
--green3: hsl(150, 50%, 60%);
|
||||||
|
--green4: hsl(150, 50%, 70%);
|
||||||
|
--green5: hsl(150, 50%, 80%);
|
||||||
|
/* Yellow accent */
|
||||||
|
--yellow0: hsl(40, 70%, 40%);
|
||||||
|
--yellow1: hsl(40, 70%, 50%);
|
||||||
|
--yellow2: hsl(40, 70%, 60%);
|
||||||
|
--yellow3: hsl(40, 70%, 70%);
|
||||||
|
--yellow4: hsl(40, 70%, 80%);
|
||||||
|
--yellow5: hsl(40, 70%, 90%);
|
||||||
|
/* Blue accent */
|
||||||
|
--blue0: hsl(240, 60%, 45%);
|
||||||
|
--blue1: hsl(240, 60%, 55%);
|
||||||
|
--blue2: hsl(240, 60%, 65%);
|
||||||
|
--blue3: hsl(240, 60%, 75%);
|
||||||
|
--blue4: hsl(240, 60%, 85%);
|
||||||
|
--blue5: hsl(240, 60%, 95%);
|
||||||
|
/* Danger */
|
||||||
|
--red0: hsl(340, 55%, 40%);
|
||||||
|
--red1: hsl(340, 55%, 50%);
|
||||||
|
--red2: hsl(340, 55%, 60%);
|
||||||
|
--red3: hsl(340, 55%, 70%);
|
||||||
|
--red4: hsl(340, 55%, 80%);
|
||||||
|
--red5: hsl(340, 55%, 90%);
|
||||||
|
/* Code */
|
||||||
|
--syntax-comment: #8b949e;
|
||||||
|
--syntax-constant: #79c0ff;
|
||||||
|
--syntax-entity: #d2a8ff;
|
||||||
|
--syntax-storage-modifier-import: #c9d1d9;
|
||||||
|
--syntax-entity-tag: #7ee787;
|
||||||
|
--syntax-keyword: #ff7b72;
|
||||||
|
--syntax-string: #a5d6ff;
|
||||||
|
--syntax-variable: #ffa657;
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkpurple .textbox,
|
||||||
|
.darkyellow .textbox,
|
||||||
|
.darkgreen .textbox,
|
||||||
|
.darkmuted .textbox,
|
||||||
|
.darkblue .textbox {
|
||||||
|
background-color: var(--b0);
|
||||||
|
border-color: var(--b0);
|
||||||
|
}
|
||||||
|
.darkpurple .textbox:focus,
|
||||||
|
.darkyellow .textbox:focus,
|
||||||
|
.darkgreen .textbox:focus,
|
||||||
|
.darkmuted .textbox:focus,
|
||||||
|
.darkblue .textbox:focus,
|
||||||
|
.darkpurple .textbox:focus-within,
|
||||||
|
.darkyellow .textbox:focus-within,
|
||||||
|
.darkgreen .textbox:focus-within,
|
||||||
|
.darkmuted .textbox:focus-within,
|
||||||
|
.darkblue .textbox:focus-within {
|
||||||
|
background-color: var(--b2);
|
||||||
|
border-color: var(--f4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkpurple .slider input[type="range"],
|
||||||
|
.darkyellow .slider input[type="range"],
|
||||||
|
.darkgreen .slider input[type="range"],
|
||||||
|
.darkmuted .slider input[type="range"],
|
||||||
|
.darkblue .slider input[type="range"] {
|
||||||
|
--thumbFill: var(--f2);
|
||||||
|
--thumbFillHover: var(--f4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkpurple .text-info,
|
||||||
|
.darkyellow .text-info,
|
||||||
|
.darkgreen .text-info,
|
||||||
|
.darkmuted .text-info,
|
||||||
|
.darkblue .text-info {
|
||||||
|
color: var(--blue2) !important;
|
||||||
|
}
|
||||||
|
.darkpurple .text-success,
|
||||||
|
.darkyellow .text-success,
|
||||||
|
.darkgreen .text-success,
|
||||||
|
.darkmuted .text-success,
|
||||||
|
.darkblue .text-success {
|
||||||
|
color: var(--green2) !important;
|
||||||
|
}
|
||||||
|
.darkpurple .text-warning,
|
||||||
|
.darkyellow .text-warning,
|
||||||
|
.darkgreen .text-warning,
|
||||||
|
.darkmuted .text-warning,
|
||||||
|
.darkblue .text-warning {
|
||||||
|
color: var(--yellow2) !important;
|
||||||
|
}
|
||||||
|
.darkpurple .text-danger,
|
||||||
|
.darkyellow .text-danger,
|
||||||
|
.darkgreen .text-danger,
|
||||||
|
.darkmuted .text-danger,
|
||||||
|
.darkblue .text-danger {
|
||||||
|
color: var(--red2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkblue {
|
||||||
|
/* Background and foreground */
|
||||||
|
--b0: hsl(230, 30%, 8%);
|
||||||
|
--b1: hsl(230, 30%, 12%);
|
||||||
|
--b2: hsl(230, 30%, 18%);
|
||||||
|
--b3: hsl(230, 30%, 20%);
|
||||||
|
--b4: hsl(220, 30%, 30%);
|
||||||
|
--b5: hsl(210, 30%, 45%);
|
||||||
|
--f4: hsl(200, 30%, 60%);
|
||||||
|
--f3: hsl(190, 40%, 75%);
|
||||||
|
--f2: hsl(190, 50%, 85%);
|
||||||
|
--f1: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkyellow {
|
||||||
|
/* Background and foreground */
|
||||||
|
--b0: hsl(40, 20%, 8%);
|
||||||
|
--b1: hsl(40, 20%, 12%);
|
||||||
|
--b2: hsl(40, 20%, 18%);
|
||||||
|
--b3: hsl(40, 30%, 20%);
|
||||||
|
--b4: hsl(30, 30%, 30%);
|
||||||
|
--b5: hsl(20, 30%, 45%);
|
||||||
|
--f4: hsl(10, 30%, 60%);
|
||||||
|
--f3: hsl(0, 40%, 75%);
|
||||||
|
--f2: hsl(0, 50%, 85%);
|
||||||
|
--f1: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkgreen {
|
||||||
|
/* Background and foreground */
|
||||||
|
--b0: hsl(170, 20%, 8%);
|
||||||
|
--b1: hsl(170, 20%, 12%);
|
||||||
|
--b2: hsl(170, 20%, 18%);
|
||||||
|
--b3: hsl(170, 30%, 20%);
|
||||||
|
--b4: hsl(160, 30%, 30%);
|
||||||
|
--b5: hsl(150, 30%, 45%);
|
||||||
|
--f4: hsl(140, 30%, 60%);
|
||||||
|
--f3: hsl(130, 40%, 75%);
|
||||||
|
--f2: hsl(130, 50%, 85%);
|
||||||
|
--f1: white;
|
||||||
|
--green0: hsl(180, 50%, 30%);
|
||||||
|
--green1: hsl(180, 50%, 40%);
|
||||||
|
--green2: hsl(180, 50%, 50%);
|
||||||
|
--green3: hsl(180, 50%, 60%);
|
||||||
|
--green4: hsl(180, 50%, 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkmuted {
|
||||||
|
/* Background and foreground */
|
||||||
|
--b0: hsl(180, 10%, 5%);
|
||||||
|
--b1: hsl(190, 10%, 8%);
|
||||||
|
--b2: hsl(200, 10%, 12%);
|
||||||
|
--b3: hsl(210, 10%, 15%);
|
||||||
|
--b4: hsl(220, 10%, 25%);
|
||||||
|
--b5: hsl(230, 10%, 40%);
|
||||||
|
--f4: hsl(240, 10%, 55%);
|
||||||
|
--f3: hsl(250, 10%, 70%);
|
||||||
|
--f2: hsl(260, 10%, 85%);
|
||||||
|
--f1: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightblue,
|
||||||
|
.lightyellow,
|
||||||
|
.lightgreen,
|
||||||
|
.lightmuted,
|
||||||
|
.lightpurple {
|
||||||
|
/* Luminance palette */
|
||||||
|
--l0: var(--f1);
|
||||||
|
--l1: var(--f2);
|
||||||
|
--l2: var(--f3);
|
||||||
|
--l3: var(--f4);
|
||||||
|
--l4: var(--b5);
|
||||||
|
--l5: var(--b4);
|
||||||
|
--l6: var(--b3);
|
||||||
|
--l7: var(--b2);
|
||||||
|
--l8: var(--b1);
|
||||||
|
--l9: var(--b0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.darkpurple,
|
||||||
|
.darkyellow,
|
||||||
|
.darkgreen,
|
||||||
|
.darkmuted,
|
||||||
|
.darkblue {
|
||||||
|
/* Luminance palette */
|
||||||
|
--l0: var(--b0);
|
||||||
|
--l1: var(--b1);
|
||||||
|
--l2: var(--b2);
|
||||||
|
--l3: var(--b3);
|
||||||
|
--l4: var(--b4);
|
||||||
|
--l5: var(--b5);
|
||||||
|
--l6: var(--f4);
|
||||||
|
--l7: var(--f3);
|
||||||
|
--l8: var(--f2);
|
||||||
|
--l9: var(--f1);
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
self.addEventListener('activate', (e) => {
|
self.addEventListener('activate', (e) => {
|
||||||
self.clients.claim();
|
self.clients.claim();
|
||||||
});
|
});
|
||||||
@ -5,44 +6,24 @@ self.addEventListener('activate', (e) => {
|
|||||||
self.addEventListener('fetch', (e) => {
|
self.addEventListener('fetch', (e) => {
|
||||||
const reqUrl = e.request.url;
|
const reqUrl = e.request.url;
|
||||||
e.respondWith((async() => {
|
e.respondWith((async() => {
|
||||||
// Open asset cache
|
// Open asset cache and see if this request is in it
|
||||||
const cache = await caches.open('main');
|
const cache = await caches.open('main');
|
||||||
const match = await caches.match(e.request);
|
const match = await caches.match(e.request);
|
||||||
|
// Request the resource from the network
|
||||||
// Request from network
|
|
||||||
const netRes = fetch(e.request).then((res) => {
|
const netRes = fetch(e.request).then((res) => {
|
||||||
// Create a new response with updated headers
|
// If the request was successful and this isn't an API call,
|
||||||
const newHeaders = new Headers(res.headers);
|
// update the cached resource
|
||||||
newHeaders.set('Permissions-Policy', 'clipboard-write=(self), clipboard-read=(self)');
|
|
||||||
|
|
||||||
const newRes = new Response(res.body, {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
headers: newHeaders
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cache non-API responses
|
|
||||||
if (res.ok && !reqUrl.match(/\/api\/sftp\/.*$/)) {
|
if (res.ok && !reqUrl.match(/\/api\/sftp\/.*$/)) {
|
||||||
cache.put(e.request, newRes.clone());
|
cache.put(e.request, res.clone());
|
||||||
}
|
}
|
||||||
|
// Return the response
|
||||||
return newRes;
|
return res;
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
console.error('Fetch error:', e);
|
console.error(e);
|
||||||
if (match) {
|
return match;
|
||||||
// Add header to cached response
|
|
||||||
const newHeaders = new Headers(match.headers);
|
|
||||||
newHeaders.set('Permissions-Policy', 'clipboard-write=(self), clipboard-read=(self)');
|
|
||||||
return new Response(match.body, {
|
|
||||||
status: match.status,
|
|
||||||
statusText: match.statusText,
|
|
||||||
headers: newHeaders
|
|
||||||
});
|
});
|
||||||
}
|
// Return the cached resource if it exists
|
||||||
throw e;
|
// Otherwise, return the network request
|
||||||
});
|
|
||||||
|
|
||||||
// Return cached or network response
|
|
||||||
return match || netRes;
|
return match || netRes;
|
||||||
})());
|
})());
|
||||||
});
|
});
|
Reference in New Issue
Block a user