mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Custom editor
This commit is contained in:
@@ -118,6 +118,46 @@
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal .modal-inner .code-editor.CodeMirror {
|
||||||
|
height: 420px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-inner .code-editor.CodeMirror-focused {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-inner .code-editor .CodeMirror-cursor {
|
||||||
|
border-left: 2px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-inner .code-editor .CodeMirror-selected {
|
||||||
|
background: rgba(124, 58, 237, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-inner .code-editor .CodeMirror-gutters {
|
||||||
|
background: #0b1220;
|
||||||
|
border-right: 1px solid #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-inner .code-editor .CodeMirror-linenumber {
|
||||||
|
color: rgba(148, 163, 184, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-inner .code-editor.CodeMirror-disabled {
|
||||||
|
background: #020617;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal .modal-inner .code-editor .CodeMirror-placeholder {
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
|
||||||
.modal .modal-inner .form-error {
|
.modal .modal-inner .form-error {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: rgba(200, 0, 0, 0.3);
|
background-color: rgba(200, 0, 0, 0.3);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function createCustomAssetModal({
|
|||||||
const logoPreview = document.getElementById("custom-asset-logo-preview");
|
const logoPreview = document.getElementById("custom-asset-logo-preview");
|
||||||
const logoClearButton = document.getElementById("custom-asset-logo-clear");
|
const logoClearButton = document.getElementById("custom-asset-logo-clear");
|
||||||
const userSourceTextArea = document.getElementById("custom-asset-code");
|
const userSourceTextArea = document.getElementById("custom-asset-code");
|
||||||
|
let codeEditor = null;
|
||||||
const formErrorWrapper = document.getElementById("custom-asset-error");
|
const formErrorWrapper = document.getElementById("custom-asset-error");
|
||||||
const jsErrorTitle = document.getElementById("js-error-title");
|
const jsErrorTitle = document.getElementById("js-error-title");
|
||||||
const jsErrorDetails = document.getElementById("js-error-details");
|
const jsErrorDetails = document.getElementById("js-error-details");
|
||||||
@@ -47,6 +48,187 @@ export function createCustomAssetModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const registerCodeEditorLint = () => {
|
||||||
|
const CodeMirror = globalThis.CodeMirror;
|
||||||
|
if (!CodeMirror?.registerHelper || CodeMirror.__customAssetLintRegistered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CodeMirror.__customAssetLintRegistered = true;
|
||||||
|
CodeMirror.registerHelper("lint", "javascript", (text) => {
|
||||||
|
const parser = globalThis.acorn;
|
||||||
|
if (!parser) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!text.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let ast;
|
||||||
|
try {
|
||||||
|
ast = parser.parse(text, {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "script",
|
||||||
|
locations: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const line = Math.max(0, (e.loc?.line || 1) - 1);
|
||||||
|
const ch = Math.max(0, e.loc?.column || 0);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
from: CodeMirror.Pos(line, ch),
|
||||||
|
to: CodeMirror.Pos(line, ch + 1),
|
||||||
|
message: e.message,
|
||||||
|
severity: "error",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasInit = false;
|
||||||
|
let hasTick = false;
|
||||||
|
|
||||||
|
const isFunctionNode = (node) =>
|
||||||
|
node && (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression");
|
||||||
|
|
||||||
|
const markFunctionName = (name) => {
|
||||||
|
if (name === "init") hasInit = true;
|
||||||
|
if (name === "tick") hasTick = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isModuleExportsMember = (node) =>
|
||||||
|
node &&
|
||||||
|
node.type === "MemberExpression" &&
|
||||||
|
node.object?.type === "Identifier" &&
|
||||||
|
node.object.name === "module" &&
|
||||||
|
node.property?.type === "Identifier" &&
|
||||||
|
node.property.name === "exports";
|
||||||
|
|
||||||
|
const checkObjectExpression = (objectExpression) => {
|
||||||
|
if (!objectExpression || objectExpression.type !== "ObjectExpression") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const property of objectExpression.properties || []) {
|
||||||
|
if (property.type !== "Property") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const keyName = property.key?.type === "Identifier" ? property.key.name : property.key?.value;
|
||||||
|
if (keyName && isFunctionNode(property.value)) {
|
||||||
|
markFunctionName(keyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const node of ast.body) {
|
||||||
|
if (node.type === "FunctionDeclaration") {
|
||||||
|
markFunctionName(node.id?.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type !== "ExpressionStatement") continue;
|
||||||
|
|
||||||
|
const expr = node.expression;
|
||||||
|
if (expr.type !== "AssignmentExpression") continue;
|
||||||
|
|
||||||
|
const left = expr.left;
|
||||||
|
const right = expr.right;
|
||||||
|
|
||||||
|
if (left.type === "Identifier" && left.name === "exports" && right.type === "ObjectExpression") {
|
||||||
|
checkObjectExpression(right);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isModuleExportsMember(left) && right.type === "ObjectExpression") {
|
||||||
|
checkObjectExpression(right);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.type === "MemberExpression" && left.property.type === "Identifier" && isFunctionNode(right)) {
|
||||||
|
if (
|
||||||
|
(left.object.type === "Identifier" && left.object.name === "exports") ||
|
||||||
|
isModuleExportsMember(left.object)
|
||||||
|
) {
|
||||||
|
markFunctionName(left.property.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const annotations = [];
|
||||||
|
if (!hasInit) {
|
||||||
|
annotations.push({
|
||||||
|
from: CodeMirror.Pos(0, 0),
|
||||||
|
to: CodeMirror.Pos(0, 1),
|
||||||
|
message: "Missing function: init",
|
||||||
|
severity: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTick) {
|
||||||
|
annotations.push({
|
||||||
|
from: CodeMirror.Pos(0, 0),
|
||||||
|
to: CodeMirror.Pos(0, 1),
|
||||||
|
message: "Missing function: tick",
|
||||||
|
severity: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotations;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createCodeEditor = () => {
|
||||||
|
const CodeMirror = globalThis.CodeMirror;
|
||||||
|
if (!CodeMirror || !userSourceTextArea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerCodeEditorLint();
|
||||||
|
codeEditor = CodeMirror.fromTextArea(userSourceTextArea, {
|
||||||
|
mode: "javascript",
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: true,
|
||||||
|
indentUnit: 2,
|
||||||
|
tabSize: 2,
|
||||||
|
gutters: ["CodeMirror-lint-markers"],
|
||||||
|
lint: true,
|
||||||
|
placeholder: userSourceTextArea.placeholder,
|
||||||
|
});
|
||||||
|
codeEditor.getWrapperElement().classList.add("code-editor");
|
||||||
|
codeEditor.setSize(null, "420px");
|
||||||
|
codeEditor.on("change", () => {
|
||||||
|
userSourceTextArea.value = codeEditor.getValue();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCodeSource = () => (codeEditor ? codeEditor.getValue() : userSourceTextArea?.value);
|
||||||
|
|
||||||
|
const setCodeValue = (value) => {
|
||||||
|
if (codeEditor) {
|
||||||
|
codeEditor.setValue(value ?? "");
|
||||||
|
codeEditor.save();
|
||||||
|
codeEditor.refresh();
|
||||||
|
} else if (userSourceTextArea) {
|
||||||
|
userSourceTextArea.value = value ?? "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCodeReadOnly = (isReadOnly) => {
|
||||||
|
if (codeEditor) {
|
||||||
|
codeEditor.setOption("readOnly", isReadOnly ? "nocursor" : false);
|
||||||
|
codeEditor.refresh();
|
||||||
|
}
|
||||||
|
if (userSourceTextArea) {
|
||||||
|
userSourceTextArea.disabled = isReadOnly;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCodePlaceholder = (placeholder) => {
|
||||||
|
if (codeEditor) {
|
||||||
|
codeEditor.setOption("placeholder", placeholder);
|
||||||
|
}
|
||||||
|
if (userSourceTextArea) {
|
||||||
|
userSourceTextArea.placeholder = placeholder;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openLaunchModal = () => {
|
const openLaunchModal = () => {
|
||||||
launchModal?.classList.remove("hidden");
|
launchModal?.classList.remove("hidden");
|
||||||
};
|
};
|
||||||
@@ -92,12 +274,13 @@ export function createCustomAssetModal({
|
|||||||
}
|
}
|
||||||
resetLogoState();
|
resetLogoState();
|
||||||
if (userSourceTextArea) {
|
if (userSourceTextArea) {
|
||||||
userSourceTextArea.value = "";
|
|
||||||
userSourceTextArea.disabled = false;
|
|
||||||
userSourceTextArea.dataset.assetId = "";
|
userSourceTextArea.dataset.assetId = "";
|
||||||
userSourceTextArea.placeholder =
|
|
||||||
"function init(context, state) {\n const { assets } = context;\n\n}\n\nfunction tick(context, state) {\n\n}\n\n// or\n// module.exports.init = (context, state) => {};\n// module.exports.tick = (context, state) => {};";
|
|
||||||
}
|
}
|
||||||
|
setCodeValue("");
|
||||||
|
setCodeReadOnly(false);
|
||||||
|
setCodePlaceholder(
|
||||||
|
"function init(context, state) {\n const { assets } = context;\n\n}\n\nfunction tick(context, state) {\n\n}\n\n// or\n// module.exports.init = (context, state) => {};\n// module.exports.tick = (context, state) => {};",
|
||||||
|
);
|
||||||
setAttachmentState(null, []);
|
setAttachmentState(null, []);
|
||||||
resetErrors();
|
resetErrors();
|
||||||
openModal();
|
openModal();
|
||||||
@@ -125,11 +308,11 @@ export function createCustomAssetModal({
|
|||||||
logoPreview.appendChild(img);
|
logoPreview.appendChild(img);
|
||||||
}
|
}
|
||||||
if (userSourceTextArea) {
|
if (userSourceTextArea) {
|
||||||
userSourceTextArea.value = "";
|
|
||||||
userSourceTextArea.placeholder = "Loading script...";
|
|
||||||
userSourceTextArea.disabled = true;
|
|
||||||
userSourceTextArea.dataset.assetId = asset.id;
|
userSourceTextArea.dataset.assetId = asset.id;
|
||||||
}
|
}
|
||||||
|
setCodeValue("");
|
||||||
|
setCodeReadOnly(true);
|
||||||
|
setCodePlaceholder("Loading script...");
|
||||||
setAttachmentState(asset.id, asset.scriptAttachments || []);
|
setAttachmentState(asset.id, asset.scriptAttachments || []);
|
||||||
openModal();
|
openModal();
|
||||||
|
|
||||||
@@ -141,23 +324,19 @@ export function createCustomAssetModal({
|
|||||||
return response.text();
|
return response.text();
|
||||||
})
|
})
|
||||||
.then((text) => {
|
.then((text) => {
|
||||||
if (userSourceTextArea) {
|
setCodeReadOnly(false);
|
||||||
userSourceTextArea.disabled = false;
|
setCodeValue(text);
|
||||||
userSourceTextArea.value = text;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (userSourceTextArea) {
|
setCodeReadOnly(false);
|
||||||
userSourceTextArea.disabled = false;
|
setCodeValue("");
|
||||||
userSourceTextArea.value = "";
|
|
||||||
}
|
|
||||||
showToast?.("Unable to load script content.", "error");
|
showToast?.("Unable to load script content.", "error");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = (formEvent) => {
|
const handleFormSubmit = (formEvent) => {
|
||||||
formEvent.preventDefault();
|
formEvent.preventDefault();
|
||||||
const src = userSourceTextArea?.value;
|
const src = getCodeSource();
|
||||||
const error = getUserJavaScriptSourceError(src);
|
const error = getUserJavaScriptSourceError(src);
|
||||||
if (error) {
|
if (error) {
|
||||||
if (jsErrorTitle) {
|
if (jsErrorTitle) {
|
||||||
@@ -169,6 +348,7 @@ export function createCustomAssetModal({
|
|||||||
if (formErrorWrapper) {
|
if (formErrorWrapper) {
|
||||||
formErrorWrapper.classList.remove("hidden");
|
formErrorWrapper.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
codeEditor?.performLint?.();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
resetErrors();
|
resetErrors();
|
||||||
@@ -222,6 +402,8 @@ export function createCustomAssetModal({
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
createCodeEditor();
|
||||||
|
|
||||||
if (launchModal) {
|
if (launchModal) {
|
||||||
launchModal.addEventListener("click", (event) => {
|
launchModal.addEventListener("click", (event) => {
|
||||||
if (event.target === launchModal) {
|
if (event.target === launchModal) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<link rel="stylesheet" href="/css/styles.css" />
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
<link rel="stylesheet" href="/css/customAssets.css" />
|
<link rel="stylesheet" href="/css/customAssets.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/lint/lint.css" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
||||||
@@ -18,6 +20,10 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/acorn@8.15.0/dist/acorn.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/acorn@8.15.0/dist/acorn.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/javascript/javascript.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/lint/lint.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/display/placeholder.min.js"></script>
|
||||||
<script src="/js/csrf.js"></script>
|
<script src="/js/csrf.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="admin-body">
|
<body class="admin-body">
|
||||||
|
|||||||
Reference in New Issue
Block a user