initial files
72
public/helpers/comparisonHelpers.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const Handlebars = require('handlebars');
|
||||
|
||||
module.exports = {
|
||||
toJSON: function(object) {
|
||||
if(typeof object === 'object') {
|
||||
return JSON.stringify(object);
|
||||
} else {
|
||||
throw 'no object type';
|
||||
}
|
||||
},
|
||||
isObject: function(value, options) {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value) ? options.fn(this) : options.inverse(this);
|
||||
},
|
||||
isArray: function(value, options) {
|
||||
return Array.isArray(value) ? options.fn(this) : options.inverse(this);
|
||||
},
|
||||
isRGB: function(value) {
|
||||
return Array.isArray(value) && (value.length === 3 || value.length === 4);
|
||||
},
|
||||
rgbString: function(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 3
|
||||
? `rgb(${value.join(',')})`
|
||||
: value.length === 4
|
||||
? `rgba(${value.join(',')})`
|
||||
: '';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
toArray: function(value) {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (typeof value === "string") return [value];
|
||||
return [];
|
||||
},
|
||||
replaceAll: function(string, pattern, replacement) {
|
||||
return new Handlebars.SafeString(string.replaceAll(pattern, replacement) || '');
|
||||
},
|
||||
equaler: function(v1, operator, v2, options) {
|
||||
switch (operator) {
|
||||
case '==':
|
||||
return (v1 == v2) ? options.fn(this) : options.inverse(this);
|
||||
case '===':
|
||||
return (v1 === v2) ? options.fn(this) : options.inverse(this);
|
||||
case '!=':
|
||||
return (v1 != v2) ? options.fn(this) : options.inverse(this);
|
||||
case '!==':
|
||||
return (v1 !== v2) ? options.fn(this) : options.inverse(this);
|
||||
case '<':
|
||||
return (v1 < v2) ? options.fn(this) : options.inverse(this);
|
||||
case '<=':
|
||||
return (v1 <= v2) ? options.fn(this) : options.inverse(this);
|
||||
case '>':
|
||||
return (v1 > v2) ? options.fn(this) : options.inverse(this);
|
||||
case '>=':
|
||||
return (v1 >= v2) ? options.fn(this) : options.inverse(this);
|
||||
case '&&':
|
||||
return (v1 && v2) ? options.fn(this) : options.inverse(this);
|
||||
case '||':
|
||||
return (v1 || v2) ? options.fn(this) : options.inverse(this);
|
||||
case 'typeof':
|
||||
return (typeof v1 === v2) ? options.fn(this) : options.inverse(this);
|
||||
case 'like':
|
||||
return (v1.indexOf(v2)) > -1 ? options.fn(this) : options.inverse(this);
|
||||
case 'includes':
|
||||
return (v1.includes(v2)) <= 0 ? options.inverse(this) : options.fn(this);
|
||||
case '%':
|
||||
return (v1 % v2) == 0 ? options.fn(this) : options.inverse(this);
|
||||
default:
|
||||
return options.inverse(this);
|
||||
}
|
||||
},
|
||||
};
|
||||
6
public/helpers/dateHelpers.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// const moment = require('moment'); // npm install moment
|
||||
// moment.locale('de');
|
||||
|
||||
module.exports = {
|
||||
dateFormat: (date, format) => dateFormat(date, format)
|
||||
};
|
||||
24
public/helpers/fileHelper.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
// objectTree: function(context, options) {
|
||||
// let ret = '';
|
||||
|
||||
// if (context === null || context === undefined) return ret;
|
||||
|
||||
// if (Array.isArray(context)) {
|
||||
// context.forEach((item, index) => {
|
||||
// ret += options.fn({ key: index, value: item });
|
||||
// });
|
||||
// } else if (typeof context === 'object') {
|
||||
// for (const key in context) {
|
||||
// if (context.hasOwnProperty(key)) {
|
||||
// ret += options.fn({ key, value: context[key] });
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // primitive Werte
|
||||
// ret += options.fn({ key: null, value: context });
|
||||
// }
|
||||
|
||||
// return ret;
|
||||
// }
|
||||
}
|
||||
145
public/helpers/iterationHelpers.js
Normal file
@@ -0,0 +1,145 @@
|
||||
module.exports = {
|
||||
objectTree: function(obj) {
|
||||
function traverse(o) {
|
||||
if (o === null || typeof o !== 'object') return o;
|
||||
|
||||
if (Array.isArray(o)) {
|
||||
// RGB oder RGBA-Erkennung: Array aus 3 oder 4 Zahlen
|
||||
if ((o.length === 3 || o.length === 4) && o.every(v => typeof v === 'number')) {
|
||||
// const rgbString = o.length === 3
|
||||
// ? `rgb(${o.join(',')})`
|
||||
// : `rgba(${o.join(',')})`;
|
||||
const rgbString = o.join(',');
|
||||
return { key: null, value: rgbString, children: null, isRGB: true };
|
||||
}
|
||||
|
||||
// normales Array
|
||||
return o.map((v, i) => ({
|
||||
key: i,
|
||||
value: typeof v !== 'object' ? v : null,
|
||||
children: typeof v === 'object' ? traverse(v) : null
|
||||
}));
|
||||
}
|
||||
|
||||
const result = [];
|
||||
for (const key in o) {
|
||||
if (o.hasOwnProperty(key)) {
|
||||
const val = o[key];
|
||||
const children = (val !== null && typeof val === 'object') ? traverse(val) : null;
|
||||
result.push({
|
||||
key,
|
||||
value: children ? null : val,
|
||||
children
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return traverse(obj);
|
||||
},
|
||||
jsonEntriesRecursive: function(obj, options) {
|
||||
const entries = [];
|
||||
|
||||
function traverse(prefix, data) {
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((item, index) => {
|
||||
const arrayKey = `${prefix}[${index}]`;
|
||||
if (typeof item === "object" && item !== null) {
|
||||
traverse(arrayKey, item); // Rekursion für Array-Objekte
|
||||
} else {
|
||||
entries.push({ key: arrayKey, value: item });
|
||||
}
|
||||
});
|
||||
} else if (typeof data === "object" && data !== null) {
|
||||
for (const key in data) {
|
||||
if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
|
||||
|
||||
const value = data[key];
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (typeof value === "object" && value !== null) {
|
||||
traverse(fullKey, value); // Rekursion für verschachtelte Objekte
|
||||
} else {
|
||||
entries.push({ key: fullKey, value });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Primitive Werte am obersten Level
|
||||
entries.push({ key: prefix, value: data });
|
||||
}
|
||||
|
||||
console.log({ key: prefix, value: data })
|
||||
}
|
||||
|
||||
traverse("", obj);
|
||||
|
||||
// Handlebars-Block Helper
|
||||
if (options.fn) {
|
||||
return options.fn(entries);
|
||||
}
|
||||
|
||||
return entries;
|
||||
},
|
||||
groupBy: function(items, keys, options) {
|
||||
// keys kann String oder Array sein
|
||||
if (!Array.isArray(keys)) keys = [keys];
|
||||
if (!keys.length) return '';
|
||||
|
||||
const key = keys[0]; // aktueller Gruppierungsschlüssel
|
||||
const remainingKeys = keys.slice(1); // restliche Schlüssel
|
||||
|
||||
// Gruppieren nach aktuellem Schlüssel
|
||||
const groups = {};
|
||||
items.forEach(item => {
|
||||
let groupKeys = item[key];
|
||||
if (!Array.isArray(groupKeys)) groupKeys = [groupKeys];
|
||||
|
||||
groupKeys.forEach(groupKey => {
|
||||
if (!groups[groupKey]) groups[groupKey] = [];
|
||||
groups[groupKey].push(item);
|
||||
});
|
||||
});
|
||||
|
||||
// Ergebnis zusammensetzen
|
||||
let result = '';
|
||||
for (const group in groups) {
|
||||
const groupItems = groups[group];
|
||||
|
||||
if (remainingKeys.length > 0) {
|
||||
// Rekursive Verschachtelung
|
||||
// Wir rufen die gleiche Helper-Funktion intern auf
|
||||
const nestedResult = Handlebars.helpers.groupBy(groupItems, remainingKeys, options);
|
||||
result += options.fn({ key: group, items: nestedResult });
|
||||
} else {
|
||||
// Basisfall: keine weiteren Keys, items direkt weitergeben
|
||||
result += options.fn({ key: group, items: groupItems });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
parseArray: function(str){
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
ifSingle: function(array, options) {
|
||||
if (Array.isArray(array) && array.length === 1) {
|
||||
return options.fn(array[0]); // Item direkt übergeben
|
||||
}
|
||||
return options.inverse(this); // else-Block
|
||||
},
|
||||
findValue: function(array, searchKey, searchValue, returnKey) {
|
||||
if (!Array.isArray(array)) return "";
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const item = array[i];
|
||||
|
||||
if (item && item[searchKey] === searchValue) {
|
||||
return item[returnKey] ?? "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
};
|
||||
BIN
public/images/app.png
Normal file
|
After Width: | Height: | Size: 729 B |
BIN
public/images/arrow_left.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/images/arrow_right.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/images/brush.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/cursor_dark.png
Normal file
|
After Width: | Height: | Size: 957 B |
BIN
public/images/cursor_light.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/images/cursor_modern.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/images/cursor_pointer_dark.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/images/cursor_pointer_light.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/images/cursor_pointer_modern.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/images/eventlog.ico
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/images/folder.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
public/images/help.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/images/notifybubble.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/plugins.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/serverinfo.png
Normal file
|
After Width: | Height: | Size: 914 B |
BIN
public/images/tutorial.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
361
public/javascript/JSON.js
Normal file
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
createJsonTree({
|
||||
container: domElement,
|
||||
data: jsonData,
|
||||
onChange: (data) => { }, // optional: callback on change
|
||||
onSave: (data) => { } // optional: if set, a save button will be added
|
||||
});
|
||||
*/function createJsonTree({
|
||||
container,
|
||||
data,
|
||||
onChange = () => {},
|
||||
onSave = null
|
||||
}) {
|
||||
container.innerHTML = "";
|
||||
container.classList.add("json-tree-root");
|
||||
|
||||
const history = [];
|
||||
const redoStack = [];
|
||||
|
||||
let lastSnapshot = clone(data);
|
||||
|
||||
function clone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function diff(oldObj, newObj) {
|
||||
const changes = {};
|
||||
|
||||
function walk(o, n, path = "") {
|
||||
const keys = new Set([
|
||||
...Object.keys(o || {}),
|
||||
...Object.keys(n || {})
|
||||
]);
|
||||
|
||||
for (const key of keys) {
|
||||
const oldVal = o?.[key];
|
||||
const newVal = n?.[key];
|
||||
|
||||
const currentPath = path ? `${path}.${key}` : key;
|
||||
|
||||
if (
|
||||
typeof oldVal === "object" &&
|
||||
oldVal !== null &&
|
||||
typeof newVal === "object" &&
|
||||
newVal !== null &&
|
||||
!Array.isArray(oldVal)
|
||||
) {
|
||||
walk(oldVal, newVal, currentPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
||||
changes[currentPath] = newVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(oldObj, newObj);
|
||||
return changes;
|
||||
}
|
||||
|
||||
function pushHistory() {
|
||||
history.push(clone(data));
|
||||
redoStack.length = 0;
|
||||
}
|
||||
|
||||
function undo() {
|
||||
if (!history.length) return;
|
||||
redoStack.push(clone(data));
|
||||
data = history.pop();
|
||||
render();
|
||||
}
|
||||
|
||||
function redo() {
|
||||
if (!redoStack.length) return;
|
||||
history.push(clone(data));
|
||||
data = redoStack.pop();
|
||||
render();
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (onSave) onSave(data);
|
||||
}
|
||||
|
||||
function autoResize(input) {
|
||||
input.style.width = "10px";
|
||||
input.style.width = input.scrollWidth + "px";
|
||||
}
|
||||
|
||||
function remove(key, parent) {
|
||||
if (key === null || parent == null) return;
|
||||
|
||||
if (!confirm("Wirklich löschen?")) return;
|
||||
|
||||
pushHistory();
|
||||
|
||||
if (Array.isArray(parent)) {
|
||||
parent.splice(key, 1);
|
||||
} else {
|
||||
delete parent[key];
|
||||
}
|
||||
|
||||
onChange(data);
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
container.innerHTML = "";
|
||||
|
||||
const controls = document.createElement("div");
|
||||
controls.className = "json-controls";
|
||||
|
||||
const undoBtn = document.createElement("button");
|
||||
undoBtn.className = "monolyth";
|
||||
undoBtn.textContent = "Undo";
|
||||
undoBtn.onclick = undo;
|
||||
|
||||
const redoBtn = document.createElement("button");
|
||||
redoBtn.className = "monolyth";
|
||||
redoBtn.textContent = "Redo";
|
||||
redoBtn.onclick = redo;
|
||||
|
||||
controls.appendChild(undoBtn);
|
||||
controls.appendChild(redoBtn);
|
||||
|
||||
if (onSave) {
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.className = "monolyth";
|
||||
saveBtn.textContent = "Save";
|
||||
saveBtn.onclick = save;
|
||||
controls.appendChild(saveBtn);
|
||||
}
|
||||
|
||||
container.appendChild(controls);
|
||||
container.appendChild(renderNode(data, null, null, "root", 0));
|
||||
}
|
||||
|
||||
function renderNode(value, key, parent, path, level) {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "json-line";
|
||||
wrapper.style.marginLeft = `${level * 18}px`;
|
||||
|
||||
const keySpan = document.createElement("span");
|
||||
keySpan.className = "json-key";
|
||||
|
||||
if (key !== null) {
|
||||
keySpan.textContent = `"${key}": `;
|
||||
}
|
||||
|
||||
const removeBtn = document.createElement("span");
|
||||
removeBtn.className = "json-remove";
|
||||
removeBtn.textContent = " [x]";
|
||||
removeBtn.onclick = (evt) => {
|
||||
evt.stopPropagation();
|
||||
remove(key, parent);
|
||||
};
|
||||
|
||||
// OBJECT / ARRAY
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const isArray = Array.isArray(value);
|
||||
|
||||
const header = document.createElement("div");
|
||||
header.className = "json-header";
|
||||
|
||||
const toggle = document.createElement("span");
|
||||
toggle.className = "json-toggle";
|
||||
toggle.textContent = isArray ? "[ ]" : "{ }";
|
||||
|
||||
const keyLabel = document.createElement("span");
|
||||
keyLabel.className = "json-key";
|
||||
if (key !== null) keyLabel.textContent = `"${key}": `;
|
||||
|
||||
const addBtn = document.createElement("span");
|
||||
addBtn.className = "json-add";
|
||||
addBtn.textContent = " [+]";
|
||||
|
||||
const children = document.createElement("div");
|
||||
children.className = "json-children";
|
||||
|
||||
keyLabel.onclick = toggle.onclick = () => {
|
||||
children.classList.toggle("collapsed");
|
||||
};
|
||||
|
||||
addBtn.onclick = () => {
|
||||
let newKey = "";
|
||||
let newValue;
|
||||
|
||||
feedbox({
|
||||
title: `<span style="color:#f44336">Key</span> hinzufügen`,
|
||||
message: `
|
||||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||||
${!isArray ? `
|
||||
<input id="newJsonKeyName" placeholder="Key Name" style="padding:6px;" />
|
||||
` : ``}
|
||||
<select id="newJsonValueType">
|
||||
<option value="text">Text</option>
|
||||
<option value="number">Zahl</option>
|
||||
<option value="boolean">Boolean</option>
|
||||
<option value="object">Objekt</option>
|
||||
<option value="array">Array</option>
|
||||
</select>
|
||||
</div>
|
||||
`,
|
||||
buttons: {
|
||||
yes: {
|
||||
text: '<b>Anlegen</b>',
|
||||
onClick: () => {
|
||||
pushHistory();
|
||||
|
||||
if (!isArray) {
|
||||
const input = document.querySelector('#newJsonKeyName');
|
||||
newKey = input?.value?.trim();
|
||||
|
||||
if (!newKey) return;
|
||||
}
|
||||
|
||||
const selectedType = document.querySelector('#newJsonValueType').value;
|
||||
|
||||
switch (selectedType) {
|
||||
case "number": newValue = 0; break;
|
||||
case "boolean": newValue = false; break;
|
||||
case "object": newValue = {}; break;
|
||||
case "array": newValue = []; break;
|
||||
default: newValue = "";
|
||||
}
|
||||
|
||||
if (isArray) {
|
||||
value.push(newValue);
|
||||
} else {
|
||||
value[newKey] = newValue;
|
||||
}
|
||||
|
||||
onChange(data);
|
||||
render();
|
||||
}
|
||||
},
|
||||
no: { text: 'Abbrechen' }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const inner = document.createElement("div");
|
||||
|
||||
const entries = isArray
|
||||
? value.map((v, i) => [i, v])
|
||||
: Object.entries(value);
|
||||
|
||||
entries.forEach(([k, v]) => {
|
||||
inner.appendChild(
|
||||
renderNode(v, k, value, `${path}.${k}`, level + 1)
|
||||
);
|
||||
});
|
||||
|
||||
children.appendChild(inner);
|
||||
|
||||
header.appendChild(keyLabel);
|
||||
header.appendChild(toggle);
|
||||
header.appendChild(addBtn);
|
||||
|
||||
if (key !== null) header.appendChild(removeBtn);
|
||||
|
||||
wrapper.appendChild(header);
|
||||
wrapper.appendChild(children);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// STRING
|
||||
if (typeof value === "string") {
|
||||
const input = document.createElement("input");
|
||||
input.className = "json-input string";
|
||||
input.value = value;
|
||||
|
||||
input.oninput = () => {
|
||||
pushHistory();
|
||||
parent[key] = input.value;
|
||||
autoResize(input);
|
||||
onChange(data);
|
||||
};
|
||||
|
||||
setTimeout(() => autoResize(input), 0);
|
||||
|
||||
wrapper.appendChild(keySpan);
|
||||
wrapper.appendChild(input);
|
||||
if (key !== null) wrapper.appendChild(removeBtn);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// NUMBER
|
||||
if (typeof value === "number") {
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.className = "json-input number";
|
||||
input.value = value;
|
||||
|
||||
input.oninput = () => {
|
||||
pushHistory();
|
||||
parent[key] = Number(input.value);
|
||||
autoResize(input);
|
||||
onChange(data);
|
||||
};
|
||||
|
||||
setTimeout(() => autoResize(input), 0);
|
||||
|
||||
wrapper.appendChild(keySpan);
|
||||
wrapper.appendChild(input);
|
||||
if (key !== null) wrapper.appendChild(removeBtn);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// BOOLEAN
|
||||
if (typeof value === "boolean") {
|
||||
const input = document.createElement("input");
|
||||
input.type = "checkbox";
|
||||
input.checked = value;
|
||||
|
||||
input.onchange = () => {
|
||||
pushHistory();
|
||||
parent[key] = input.checked;
|
||||
onChange(data);
|
||||
};
|
||||
|
||||
wrapper.appendChild(keySpan);
|
||||
wrapper.appendChild(input);
|
||||
if (key !== null) wrapper.appendChild(removeBtn);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.textContent = String(value);
|
||||
|
||||
wrapper.appendChild(keySpan);
|
||||
wrapper.appendChild(span);
|
||||
if (key !== null) wrapper.appendChild(removeBtn);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
pushHistory();
|
||||
render();
|
||||
|
||||
return {
|
||||
undo,
|
||||
redo,
|
||||
save,
|
||||
refresh: render,
|
||||
getData: () => data,
|
||||
getChanges() {
|
||||
return diff(lastSnapshot, data);
|
||||
},
|
||||
commit() {
|
||||
lastSnapshot = clone(data);
|
||||
},
|
||||
update(newData) {
|
||||
data = newData;
|
||||
lastSnapshot = clone(newData);
|
||||
pushHistory();
|
||||
render();
|
||||
}
|
||||
};
|
||||
}
|
||||
244
public/javascript/contextMenu.js
Normal file
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
const ctx = new ContextMenu();
|
||||
|
||||
ctx.setItems([
|
||||
{ label: "Öffnen", disable: true, onClick: () => alert("Öffnen!") },
|
||||
{ label: "Öffnen", onClick: () => alert("Öffnen!") },
|
||||
{ label: "Bearbeiten", onClick: () => alert("Bearbeiten…") },
|
||||
{ type: "divider" },
|
||||
{
|
||||
label: "Mehr Optionen",
|
||||
color: 'rgb()|color-name|color-value',
|
||||
children: [
|
||||
{ label: "Duplizieren", onClick: () => alert("Dupliziert!") },
|
||||
{ label: "Umbenennen", onClick: () => alert("Rename…") },
|
||||
{
|
||||
label: "Exportieren",
|
||||
children: [
|
||||
{ label: "PDF", onClick: () => alert("PDF export") },
|
||||
{ label: "CSV", onClick: () => alert("CSV export") },
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ label: "Löschen", onClick: () => alert("Gelöscht!") },
|
||||
]);
|
||||
|
||||
ctx.show(element, or location, { position: "right" });
|
||||
*/
|
||||
|
||||
|
||||
class ContextMenu {
|
||||
constructor() {
|
||||
this.menu = document.createElement("div");
|
||||
this.menu.className = "ctx-menu hidden";
|
||||
document.body.appendChild(this.menu);
|
||||
|
||||
// close on click outside → sofort
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!this.menu.contains(e.target)) {
|
||||
this.closeAllSubmenus();
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("resize", () => this.hide());
|
||||
window.addEventListener("scroll", () => this.hide());
|
||||
}
|
||||
|
||||
setItems(items) {
|
||||
this.menu.innerHTML = "";
|
||||
this.menu.appendChild(this.buildMenu(items));
|
||||
}
|
||||
|
||||
buildMenu(items) {
|
||||
const ul = document.createElement("ul");
|
||||
ul.className = "ctx-list";
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.type === "divider") {
|
||||
const divider = document.createElement("li");
|
||||
divider.className = "ctx-divider";
|
||||
ul.appendChild(divider);
|
||||
return;
|
||||
}
|
||||
|
||||
const li = document.createElement("li");
|
||||
li.className = "ctx-item";
|
||||
|
||||
if (item.color) li.style.boxShadow = `inset 5px 0px 0px 0px ${item.color}`;
|
||||
if (item.disabled) li.classList.add("ctx-disabled");
|
||||
|
||||
li.innerHTML = `
|
||||
<span class="ctx-label">${item.label}</span>
|
||||
${item.children ? "<span class='ctx-arrow'>▶</span>" : ""}
|
||||
`;
|
||||
|
||||
if (item.onClick && !item.children && !item.disabled) {
|
||||
li.addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
item.onClick(e);
|
||||
this.hide(); // Hauptmenü sofort schließen
|
||||
});
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
const submenu = this.buildMenu(item.children);
|
||||
submenu.classList.add("ctx-submenu");
|
||||
li.appendChild(submenu);
|
||||
|
||||
let hideTimeout;
|
||||
|
||||
const openSubmenu = () => {
|
||||
clearTimeout(hideTimeout);
|
||||
submenu.classList.add("open");
|
||||
|
||||
submenu.style.left = "";
|
||||
submenu.style.top = "";
|
||||
|
||||
const liRect = li.getBoundingClientRect();
|
||||
const submenuRect = submenu.getBoundingClientRect();
|
||||
|
||||
let left = li.offsetWidth;
|
||||
if (liRect.right + submenuRect.width > window.innerWidth) left = -submenuRect.width;
|
||||
submenu.style.left = left + "px";
|
||||
|
||||
let top = 0;
|
||||
if (liRect.top + submenuRect.height > window.innerHeight) {
|
||||
top = window.innerHeight - (liRect.top + submenuRect.height) - 4;
|
||||
}
|
||||
submenu.style.top = top + "px";
|
||||
};
|
||||
|
||||
const closeSubmenu = (delay = 500) => {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = setTimeout(() => {
|
||||
submenu.classList.remove("open");
|
||||
submenu.style.left = "";
|
||||
submenu.style.top = "";
|
||||
}, delay);
|
||||
};
|
||||
|
||||
li.addEventListener("mouseenter", openSubmenu);
|
||||
li.addEventListener("mouseleave", () => closeSubmenu(500));
|
||||
submenu.addEventListener("mouseenter", () => clearTimeout(hideTimeout));
|
||||
submenu.addEventListener("mouseleave", () => closeSubmenu(500));
|
||||
}
|
||||
|
||||
ul.appendChild(li);
|
||||
});
|
||||
|
||||
return ul;
|
||||
}
|
||||
|
||||
closeAllSubmenus() {
|
||||
this.menu.querySelectorAll(".ctx-submenu.open").forEach(sm => {
|
||||
sm.classList.remove("open");
|
||||
sm.style.left = "";
|
||||
sm.style.top = "";
|
||||
});
|
||||
}
|
||||
|
||||
show(target, options = {}) {
|
||||
this.closeAllSubmenus();
|
||||
|
||||
let x, y;
|
||||
|
||||
const position = options.position || "right"; // default
|
||||
const offset = options.offset || 4;
|
||||
|
||||
// 🖱️ Mausposition
|
||||
if (typeof target === "number") {
|
||||
x = target;
|
||||
y = options.y;
|
||||
}
|
||||
|
||||
// 📦 Element als Anchor
|
||||
else if (target instanceof HTMLElement) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
|
||||
switch (position) {
|
||||
case "right":
|
||||
x = rect.right + offset;
|
||||
y = rect.top;
|
||||
break;
|
||||
|
||||
case "left":
|
||||
x = rect.left - this.menu.offsetWidth - offset;
|
||||
y = rect.top;
|
||||
break;
|
||||
|
||||
case "top":
|
||||
x = rect.left;
|
||||
y = rect.top - this.menu.offsetHeight - offset;
|
||||
break;
|
||||
|
||||
case "bottom":
|
||||
default:
|
||||
x = rect.left;
|
||||
y = rect.bottom + offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ invalid fallback
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
// 👉 erstmal anzeigen (wichtig für width/height!)
|
||||
this.menu.style.left = "0px";
|
||||
this.menu.style.top = "0px";
|
||||
this.menu.classList.add("show");
|
||||
this.menu.classList.remove("hidden");
|
||||
|
||||
const rect = this.menu.getBoundingClientRect();
|
||||
|
||||
// 🧠 Reposition nach echten Maßen
|
||||
if (target instanceof HTMLElement) {
|
||||
const anchor = target.getBoundingClientRect();
|
||||
|
||||
switch (position) {
|
||||
case "right":
|
||||
x = anchor.right + offset;
|
||||
y = anchor.top;
|
||||
break;
|
||||
|
||||
case "left":
|
||||
x = anchor.left - rect.width - offset;
|
||||
y = anchor.top;
|
||||
break;
|
||||
|
||||
case "top":
|
||||
x = anchor.left;
|
||||
y = anchor.top - rect.height - offset;
|
||||
break;
|
||||
|
||||
case "bottom":
|
||||
x = anchor.left;
|
||||
y = anchor.bottom + offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 🧱 Screen Bounds
|
||||
if (x + rect.width > window.innerWidth) {
|
||||
x = window.innerWidth - rect.width - 4;
|
||||
}
|
||||
|
||||
if (y + rect.height > window.innerHeight) {
|
||||
y = window.innerHeight - rect.height - 4;
|
||||
}
|
||||
|
||||
if (x < 0) x = 4;
|
||||
if (y < 0) y = 4;
|
||||
|
||||
this.menu.style.left = x + "px";
|
||||
this.menu.style.top = y + "px";
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.menu.classList.remove("show");
|
||||
setTimeout(() => this.menu.classList.add("hidden"), 200);
|
||||
}
|
||||
}
|
||||
259
public/javascript/customModal.js
Normal file
@@ -0,0 +1,259 @@
|
||||
let activeFeedbox = null;
|
||||
|
||||
|
||||
//#region Messagebox
|
||||
function getMessageColorByLevelId(levelId) {
|
||||
return levelId == -1 ? 'test' :
|
||||
levelId == 0 ? 'success' :
|
||||
levelId == 1 ? 'info' :
|
||||
levelId == 2 ? 'warn' :
|
||||
levelId == 4 ? 'error' : 'throw_exception';
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows messages in a containerbox on the right
|
||||
* @param {string} title - Title
|
||||
* @param {string} text - Message content
|
||||
* @param {string} levelId - 1=info | 2=warn | 4=error | 8=throw_exception | 16=success
|
||||
* @param {string} [targetId] - Which element is getting focused
|
||||
* @param {number} duration - Time in ms until auto-close
|
||||
*/
|
||||
// function showMessage(title, text, levelId = 1, targetId = null, duration = 4000) {
|
||||
function showMessage(title, text, levelId = 1, onclick = null, duration = 4000) {
|
||||
// Falls kein Container existiert → automatisch anlegen
|
||||
let container = document.getElementById('message-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'message-container';
|
||||
container.style.position = 'fixed';
|
||||
container.style.top = '20px';
|
||||
container.style.right = '20px';
|
||||
container.style.display = 'flex';
|
||||
container.style.flexDirection = 'column';
|
||||
container.style.alignItems = 'flex-end';
|
||||
container.style.zIndex = '9999';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
const message = document.createElement('div');
|
||||
message.classList.add('message', getMessageColorByLevelId(levelId));
|
||||
|
||||
// --- HEADER ---
|
||||
const header = document.createElement('div');
|
||||
header.className = 'message-header';
|
||||
|
||||
const titleContainer = document.createElement('div');
|
||||
titleContainer.className = 'message-title';
|
||||
titleContainer.innerHTML = title;
|
||||
|
||||
const pinDiv = document.createElement('div');
|
||||
pinDiv.className = 'pin-div';
|
||||
pinDiv.innerHTML = '📌';
|
||||
pinDiv.title = 'Anpinnen';
|
||||
|
||||
const countdown = document.createElement('span');
|
||||
countdown.className = 'countdown';
|
||||
countdown.textContent = `${(duration / 1000).toFixed(1)}s`;
|
||||
|
||||
// Titel + Countdown + Pin in eine Zeile
|
||||
header.appendChild(titleContainer);
|
||||
header.appendChild(countdown);
|
||||
header.appendChild(pinDiv);
|
||||
|
||||
// --- BODY ---
|
||||
const body = document.createElement('div');
|
||||
body.className = 'message-text';
|
||||
body.innerHTML = text;
|
||||
|
||||
message.appendChild(header);
|
||||
message.appendChild(body);
|
||||
container.appendChild(message);
|
||||
|
||||
// --- LOGIK ---
|
||||
let pinned = false;
|
||||
let remainingTime = duration;
|
||||
let interval = null;
|
||||
let lastTick = Date.now();
|
||||
|
||||
function startCountdown() {
|
||||
lastTick = Date.now();
|
||||
clearInterval(interval);
|
||||
interval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const delta = now - lastTick;
|
||||
lastTick = now;
|
||||
remainingTime -= delta;
|
||||
|
||||
if (remainingTime <= 0 && !pinned) {
|
||||
clearInterval(interval);
|
||||
slideOutMessage(message);
|
||||
} else if (!pinned) {
|
||||
countdown.textContent = `${(remainingTime / 1000).toFixed(1)}s`;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function stopCountdown() {
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
// Start Countdown
|
||||
startCountdown();
|
||||
|
||||
// Hover pausiert Countdown
|
||||
message.addEventListener('mouseenter', stopCountdown);
|
||||
message.addEventListener('mouseleave', () => {
|
||||
if (!pinned) startCountdown();
|
||||
});
|
||||
|
||||
// Klick auf Nachricht → schließen (nur wenn nicht gepinnt)
|
||||
message.addEventListener('click', (e) => {
|
||||
if(onclick) {
|
||||
onclick();
|
||||
}
|
||||
// if (e.target === pinDiv) return;
|
||||
// if (targetId) {
|
||||
// const target = document.getElementById(targetId);
|
||||
// if (target) target.scrollIntoView({ behavior: 'smooth' });
|
||||
// }
|
||||
if (!pinned) slideOutMessage(message);
|
||||
});
|
||||
|
||||
// Klick auf Pin → anheften / lösen
|
||||
pinDiv.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
pinned = !pinned;
|
||||
if (pinned) {
|
||||
pinDiv.classList.add('pinned');
|
||||
pinDiv.title = 'Loslösen';
|
||||
stopCountdown();
|
||||
countdown.textContent = '📍 Angeheftet';
|
||||
} else {
|
||||
pinDiv.classList.remove('pinned');
|
||||
pinDiv.title = 'Anpinnen';
|
||||
startCountdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function slideOutMessage(message) {
|
||||
if (!message) return;
|
||||
message.style.animation = 'slideOut 0.5s forwards';
|
||||
setTimeout(() => {
|
||||
if (message.parentNode) message.parentNode.removeChild(message);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
|
||||
//#region Feedbox
|
||||
|
||||
/**
|
||||
* feedbox({
|
||||
* title: `<span style="color:#f44336">⚠ Upload abbrechen?</span>`,
|
||||
* message: `
|
||||
* <p>Es laufen noch <b>aktive Uploads</b>.</p>
|
||||
* <p>Möchtest du wirklich <u>alle abbrechen</u>?</p>
|
||||
* `,
|
||||
* buttons: {
|
||||
* yes: {
|
||||
* text: '<b>Ja</b>, abbrechen',
|
||||
* onClick: () => stopUploadQueue()
|
||||
* },
|
||||
* no: {
|
||||
* text: 'Weiter hochladen'
|
||||
* },
|
||||
* cancel: {
|
||||
* text: 'Zurück'
|
||||
* }
|
||||
* }
|
||||
*});
|
||||
*/
|
||||
function feedbox({
|
||||
title = '',
|
||||
message = '',
|
||||
buttons = {}, // buttons: { yes: { text: 'Yes', onClick: () => { } } }
|
||||
primary = null, // name of the primary button, to accept with enter-key
|
||||
lock = false, // locks desktop
|
||||
replace = false // replaces an actually shown feedbox
|
||||
}) {
|
||||
// 🚫 Nested verhindern
|
||||
if (activeFeedbox) {
|
||||
if (!replace) {
|
||||
return Promise.resolve('blocked');
|
||||
}
|
||||
activeFeedbox.close('replaced');
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'feedbox-overlay';
|
||||
|
||||
const box = document.createElement('div');
|
||||
box.className = 'feedbox';
|
||||
|
||||
const h = document.createElement('h3');
|
||||
h.innerHTML = title;
|
||||
|
||||
const msg = document.createElement('div');
|
||||
msg.className = 'feedbox-message';
|
||||
msg.innerHTML = message;
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'feedbox-actions';
|
||||
|
||||
const btnMap = {};
|
||||
|
||||
Object.entries(buttons).forEach(([key, cfg]) => {
|
||||
if (!cfg) return;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `feedbox-btn ${key}`;
|
||||
btn.innerHTML = cfg.text ?? key;
|
||||
|
||||
btn.onclick = () => {
|
||||
cfg.onClick?.();
|
||||
close(key);
|
||||
};
|
||||
|
||||
btnMap[key] = btn;
|
||||
actions.appendChild(btn);
|
||||
});
|
||||
|
||||
function close(result) {
|
||||
document.removeEventListener('keydown', keyHandler);
|
||||
overlay.remove();
|
||||
activeFeedbox = null;
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
function keyHandler(e) {
|
||||
if (e.key === 'Escape') close('cancel');
|
||||
if (e.key === 'Enter' && primary && btnMap[primary]) {
|
||||
btnMap[primary].click();
|
||||
}
|
||||
}
|
||||
|
||||
if (!lock) {
|
||||
overlay.onclick = e => {
|
||||
if (e.target === overlay) close('cancel');
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyHandler);
|
||||
|
||||
box.append(h, msg, actions);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Fokus
|
||||
if (primary && btnMap[primary]) {
|
||||
setTimeout(() => btnMap[primary].focus(), 0);
|
||||
}
|
||||
|
||||
// 🔐 Singleton setzen
|
||||
activeFeedbox = { close };
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
97
public/javascript/loadOnce.js
Normal file
@@ -0,0 +1,97 @@
|
||||
const auth = { objectGuid: getCookie('ObjectGUID'), sAMAccountName: getCookie('sAMAccountName') };
|
||||
const mainSocket = io.connect('/', { reconnect: true, auth: auth });
|
||||
const adminSocket = io.connect('/admin', { reconnect: true, auth: auth });
|
||||
|
||||
const container = document.getElementById('message-container');
|
||||
|
||||
|
||||
adminSocket.on('eventlog', (data) => {
|
||||
showMessage('EventLog',
|
||||
`${data.datetime}<br>${[-1,0].includes(data.levelId) ? '' : data.trace}<br><br>${(Array.isArray(data.message) ? data.message.split('\r\n').join('<br>').split('\t').join(' ') : data.message.split('\r\n').join('<br>').split('\t').join(' '))}`,
|
||||
data.levelId,
|
||||
() => {
|
||||
|
||||
} ,
|
||||
10000);
|
||||
});
|
||||
|
||||
mainSocket.on('event', (data) => {
|
||||
showMessage(data.pluginName,
|
||||
`[${data.datetime}]<br><br>${(Array.isArray(data.message) ? data.message.split('\r\n').join('<br>').split('\t').join(' ') : data.message.split('\r\n').join('<br>').split('\t').join(' '))}`,
|
||||
data.levelId,
|
||||
() => {
|
||||
|
||||
} ,
|
||||
10000);
|
||||
});
|
||||
|
||||
|
||||
|
||||
//-1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception
|
||||
function writeEventLog(levelId, pluginName, message) {
|
||||
adminSocket.emit('eventlog', {
|
||||
objectGuid: getCookie('ObjectGUID'),
|
||||
levelId: levelId,
|
||||
pluginName: pluginName,
|
||||
message: message.stack === undefined ? message : { message: message.message, stack: message.stack }
|
||||
});
|
||||
}
|
||||
|
||||
// levelId: if -1, then write no log entry
|
||||
// sendToParams: where clause to find objectGUIDs to send
|
||||
function sendUserEvent(pluginName, message, sendToParams, levelId = -1) {
|
||||
mainSocket.emit('event', {
|
||||
objectGuid: getCookie('ObjectGUID'),
|
||||
levelId: levelId,
|
||||
pluginName: pluginName,
|
||||
message: message.stack === undefined ? message : { message: message.message },
|
||||
sendToParams: sendToParams
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
{
|
||||
"status":"unload",
|
||||
"pluginName":"user_management",
|
||||
"metadata":{
|
||||
"name":"user_management",
|
||||
"menuName":"User Management",
|
||||
"description":"Beschreibung hier einfügen",
|
||||
"version":"0.9.25.11.14",
|
||||
"icon":"",
|
||||
"permissions":[],
|
||||
"config":{
|
||||
},
|
||||
"active":false
|
||||
},
|
||||
"levelId":0,
|
||||
"message":"Plugin user_management entladen",
|
||||
"authorized": true
|
||||
}
|
||||
*/
|
||||
mainSocket.on('plugin_status', payload => {
|
||||
const startMenuItem = document.querySelector(`[data-appname=${payload.metadata.name}]`);
|
||||
if(['load', 'unload', 'update'].includes(payload.status)) {
|
||||
if(payload.status == 'load') {
|
||||
if(payload.authorized) {
|
||||
startMenuItem.classList.remove('unload');
|
||||
startMenuItem.setAttribute('data-active', true);
|
||||
} else {
|
||||
startMenuItem.classList.add('unload');
|
||||
startMenuItem.setAttribute('data-active', false);
|
||||
}
|
||||
} else if(payload.status == 'unload') {
|
||||
startMenuItem.classList.add('unload');
|
||||
startMenuItem.setAttribute('data-active', false);
|
||||
} else if(payload.status == 'update') {
|
||||
if(payload.authorized && payload.metadata.active) {
|
||||
startMenuItem.classList.remove('unload');
|
||||
startMenuItem.setAttribute('data-active', true);
|
||||
} else if(!payload.authorized){
|
||||
startMenuItem.classList.add('unload');
|
||||
startMenuItem.setAttribute('data-active', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
1494
public/javascript/main.js
Normal file
157
public/javascript/notifyBubble.js
Normal file
@@ -0,0 +1,157 @@
|
||||
class NotifyBubble {
|
||||
constructor(trayButton, bubbleSelector) {
|
||||
this.button = trayButton;
|
||||
this.bubble = document.querySelector(bubbleSelector);
|
||||
this.hideTimeout = null;
|
||||
this.initEvents();
|
||||
this.initExistingItems();
|
||||
this.updateCounter(); // 🔥 initialer Counter
|
||||
}
|
||||
|
||||
/* =========================
|
||||
SHOW / HIDE
|
||||
========================== */
|
||||
show() {
|
||||
clearTimeout(this.hideTimeout);
|
||||
this.button.classList.add("active");
|
||||
|
||||
// Tooltip entfernen beim Öffnen
|
||||
this.button.removeAttribute('data-tooltip');
|
||||
}
|
||||
|
||||
hide(delay = 1000) {
|
||||
clearTimeout(this.hideTimeout);
|
||||
|
||||
this.hideTimeout = setTimeout(() => {
|
||||
this.button.classList.remove("active");
|
||||
|
||||
// 🔥 Counter nach Schließen anzeigen
|
||||
this.updateCounter();
|
||||
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
COUNTER / TOOLTIP
|
||||
========================== */
|
||||
updateCounter() {
|
||||
const items = this.bubble.querySelectorAll(".bubble-item");
|
||||
const count = items.length;
|
||||
|
||||
if (!this.button.classList.contains("active") && count > 0) {
|
||||
this.button.setAttribute('data-tooltip', `${count} neue Benachrichtigung${count > 1 ? 'en' : ''}`);
|
||||
} else {
|
||||
this.button.setAttribute('data-tooltip', "Keine Benachrichtigungen");
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
EVENTS
|
||||
========================== */
|
||||
initEvents() {
|
||||
// Toggle bei Klick auf Button
|
||||
this.button.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.button.classList.contains("active")) {
|
||||
this.hide(0);
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
});
|
||||
|
||||
// Klick innerhalb der Bubble soll NICHT schließen
|
||||
this.bubble.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Klick irgendwo anders → schließen
|
||||
document.addEventListener("click", () => {
|
||||
if (this.button.classList.contains("active")) {
|
||||
this.hide(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================
|
||||
INIT EXISTING ITEMS
|
||||
========================== */
|
||||
initExistingItems() {
|
||||
this.bubble.querySelectorAll(".bubble-item").forEach(item => {
|
||||
this.attachItemEvents(item);
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================
|
||||
ITEM EVENTS
|
||||
========================== */
|
||||
attachItemEvents(item) {
|
||||
item.addEventListener("click", () => {
|
||||
const checkbox = item.querySelector("input[type='checkbox']");
|
||||
if (checkbox) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
|
||||
fetch('/api/NotifyTray/markAsSeen', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: item.dataset.id,
|
||||
value: checkbox.checked
|
||||
})
|
||||
});
|
||||
|
||||
// 🔥 Counter ggf. aktualisieren
|
||||
this.updateCounter();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================
|
||||
ADD SINGLE ITEM
|
||||
========================== */
|
||||
addItem(notification) {
|
||||
const item = document.createElement("label");
|
||||
item.className = "bubble-item";
|
||||
item.dataset.id = notification.ID;
|
||||
|
||||
item.innerHTML = `
|
||||
<div>${notification.Message}</div>
|
||||
|
||||
<div style="display:flex;flex-direction:row;flex-wrap:no-wrap;justify-content:flex-end;gap:10px;align-items:center;">
|
||||
<div>
|
||||
<div>${dateFormat(notification.CreatedAt, 'dd.mm.yyyy HH:MM:SS')}</div>
|
||||
<div>${notification.PluginName}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="cb cb-modern">
|
||||
<input id="notification_${notification.ID}" type="checkbox">
|
||||
<span class="cb-box"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.attachItemEvents(item);
|
||||
this.bubble.appendChild(item);
|
||||
|
||||
// 🔥 Counter aktualisieren
|
||||
this.updateCounter();
|
||||
}
|
||||
|
||||
/* =========================
|
||||
ADD MULTIPLE ITEMS
|
||||
========================== */
|
||||
addItems(notifications) {
|
||||
notifications.forEach(n => this.addItem(n));
|
||||
}
|
||||
|
||||
/* =========================
|
||||
CLEAR
|
||||
========================== */
|
||||
clear() {
|
||||
this.bubble.innerHTML = "";
|
||||
this.updateCounter();
|
||||
}
|
||||
}
|
||||
1081
public/javascript/os.js
Normal file
92
public/javascript/pluginAPI.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const pluginAPI = {
|
||||
async update(name, updates) {
|
||||
if(Object.keys(updates)[0] && Object.values(updates)[0] == "") {
|
||||
updates[Object.keys(updates)[0]] = [ ];
|
||||
}
|
||||
return await this._request(
|
||||
`/api/plugins/${name}/update`,
|
||||
'POST',
|
||||
{ updates }
|
||||
);
|
||||
},
|
||||
|
||||
async activation(name, state) {
|
||||
return await this._request(
|
||||
`/api/plugins/activation`,
|
||||
'POST',
|
||||
{ name, state }
|
||||
);
|
||||
},
|
||||
|
||||
async create(name) {
|
||||
return await this._request(
|
||||
`/api/plugins/${name}/create`,
|
||||
'POST'
|
||||
);
|
||||
},
|
||||
|
||||
async rename(name, newName) {
|
||||
return await this._request(
|
||||
`/api/plugins/${name}/rename`,
|
||||
'POST',
|
||||
{ newName }
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
async delete(name) {
|
||||
return await this._request(
|
||||
`/api/plugins/${name}/delete`,
|
||||
'POST'
|
||||
);
|
||||
},
|
||||
|
||||
// --- zentrale Request-Funktion ---
|
||||
async _request(url, method, body) {
|
||||
try {
|
||||
const options = { method };
|
||||
|
||||
if (body) {
|
||||
options.headers = { 'Content-Type': 'application/json' };
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const res = await fetch(url, options);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
writeEventLog(4, 'CLIENT', `Request fehlgeschlagen: ${text}`);
|
||||
throw new Error(`HTTP ${res.status}: ${text}`);
|
||||
}
|
||||
return res.json();
|
||||
} catch (err) {
|
||||
writeEventLog(4, 'CLIENT', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
class AttachOnBlurChange {
|
||||
constructor(el, callback) {
|
||||
this.el = el;
|
||||
this.callback = callback;
|
||||
this.initial = this.el.value;
|
||||
|
||||
this.el.addEventListener('blur', () => {
|
||||
this.handleBlur();
|
||||
});
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
if (this.el.value === this.initial) return;
|
||||
|
||||
const oldValue = this.initial;
|
||||
const newValue = this.el.value;
|
||||
|
||||
this.initial = newValue;
|
||||
|
||||
this.callback(newValue, oldValue, this.el);
|
||||
}
|
||||
}
|
||||
89
public/javascript/requiredFields.js
Normal file
@@ -0,0 +1,89 @@
|
||||
function createRequiredProgress({ container, progress, onChange }) {
|
||||
let total = 0;
|
||||
let filled = 0;
|
||||
|
||||
const body = typeof container === 'string'
|
||||
? document.querySelector(container)
|
||||
: container;
|
||||
|
||||
const progressEl = typeof progress === 'string'
|
||||
? document.querySelector(progress)
|
||||
: progress;
|
||||
|
||||
if (!body || !progressEl) return;
|
||||
|
||||
function isElementVisible(el) {
|
||||
const style = window.getComputedStyle(el);
|
||||
return el.offsetParent !== null && style.opacity !== '0';
|
||||
}
|
||||
|
||||
function updateRequiredProgress() {
|
||||
const requiredElements = Array.from(
|
||||
body.querySelectorAll('input[required], select[required], textarea[required]')
|
||||
).filter(isElementVisible);
|
||||
|
||||
total = requiredElements.length;
|
||||
filled = requiredElements.filter(el =>
|
||||
el.classList.contains('is-required-filled')
|
||||
).length;
|
||||
|
||||
progressEl.textContent = `${filled} / ${total} Pflichtfelder ausgefüllt`;
|
||||
progressEl.classList.toggle('complete', filled === total);
|
||||
progressEl.classList.toggle('incomplete', filled !== total);
|
||||
}
|
||||
|
||||
function updateRequiredState(el) {
|
||||
const isEmpty =
|
||||
(el instanceof HTMLSelectElement && !el.value) ||
|
||||
(el instanceof HTMLInputElement && el.type !== 'checkbox' && !el.value.trim()) ||
|
||||
(el instanceof HTMLTextAreaElement && !el.value.trim());
|
||||
|
||||
el.classList.toggle('is-required-empty', isEmpty);
|
||||
el.classList.toggle('is-required-filled', !isEmpty);
|
||||
|
||||
updateRequiredProgress();
|
||||
|
||||
if (typeof onChange === 'function') {
|
||||
onChange({
|
||||
element: el,
|
||||
isEmpty,
|
||||
isFilled: !isEmpty,
|
||||
isFinished: total === filled
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
const elements = Array.from(
|
||||
body.querySelectorAll('input[required], select[required], textarea[required]')
|
||||
).filter(isElementVisible);
|
||||
|
||||
elements.forEach(el => {
|
||||
updateRequiredState(el);
|
||||
|
||||
el.addEventListener('input', () => updateRequiredState(el));
|
||||
el.addEventListener('change', () => updateRequiredState(el));
|
||||
});
|
||||
|
||||
updateRequiredProgress();
|
||||
}
|
||||
|
||||
// 🔥 Reagiere auf Sichtbarkeitsänderungen
|
||||
const observer = new MutationObserver(updateRequiredProgress);
|
||||
observer.observe(body, {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
attributeFilter: ['style', 'class', 'hidden']
|
||||
});
|
||||
|
||||
initialize();
|
||||
|
||||
// Optional: API zurückgeben
|
||||
return {
|
||||
initialize: initialize,
|
||||
refresh: updateRequiredProgress,
|
||||
destroy() {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
}
|
||||
450
public/javascript/tableFilter.js
Normal file
@@ -0,0 +1,450 @@
|
||||
/*
|
||||
//Generic table filter module
|
||||
// Usage:
|
||||
TableFilter({
|
||||
table: document.querySelector('#myTable'),
|
||||
exceptedColumns: [ 'Status_ID' ],
|
||||
filterConfig: {
|
||||
columnModes: {
|
||||
ID: 'text', // Textsuche in dieser Spalte
|
||||
Status: 'dropdown', // Dropdown-Werte aus Tabelle sammeln
|
||||
Objekt: 'text', // Textsuche
|
||||
Priorität: 'dropdown',
|
||||
Gewerk: 'dropdown',
|
||||
Typ: 'dropdown',
|
||||
Bedarfsmelder: 'text'
|
||||
Bearbeiter: 'text'
|
||||
Genehmiger: 'text'
|
||||
},
|
||||
checkboxFilter: {
|
||||
column: 'Status_ID',
|
||||
rules: [
|
||||
{ label: 'Bearbeitung', test: v => [1,4,6,7,12,13,14].includes(parseInt(v)) },
|
||||
{ label: 'Genehmigt', test: v => [2,5,9,10,11].includes(parseInt(v)) },
|
||||
{ label: 'Abgelehnt', test: v => [3].includes(parseInt(v)) },
|
||||
{ label: 'Abgeschlossen', test: v => [8].includes(parseInt(v)) }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
// Generic table filter module (one textbox OR column-based dropdown)
|
||||
// New features:
|
||||
// - Selecting a column in the column-selector decides: textbox OR dropdown (configured in options)
|
||||
// - If column-selector = empty value ⇒ full-row search
|
||||
// - No extra checkbox needed
|
||||
// - UI stays compact (only ONE input element + ONE dropdown)
|
||||
*/
|
||||
function TableFilter(options) {
|
||||
try {
|
||||
|
||||
const table = options.table;
|
||||
const cfg = options.filterConfig;
|
||||
const exceptedColumns = options.exceptedColumns || [];
|
||||
|
||||
// Gruppenerkennung + Verwaltung
|
||||
const groupMap = new Map(); // groupRow → childRows[]
|
||||
let groupsInitialized = false;
|
||||
|
||||
const state = {
|
||||
selectedColumn: '',
|
||||
searchText: '',
|
||||
dropdownValue: '',
|
||||
dropdownCache: {},
|
||||
checkbox: new Set(),
|
||||
};
|
||||
|
||||
//------------------------------------
|
||||
// Sticky Container für Filter + Header
|
||||
//------------------------------------
|
||||
if(document.querySelectorAll('.table-filter-container').length > 0) {
|
||||
// Array.from(document.querySelectorAll('.table-filter-container')).forEach(filterContainer => {
|
||||
// filterContainer.remove();
|
||||
// })
|
||||
const existing = table.previousElementSibling;
|
||||
if (existing && existing.classList.contains('table-filter-container')) {
|
||||
existing.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'table-filter-container';
|
||||
container.style.position = 'sticky';
|
||||
container.style.top = '0px';
|
||||
container.style.left = '0px';
|
||||
container.style.zIndex = 20;
|
||||
|
||||
// Filter UI wird hier reingesetzt
|
||||
table.before(container);
|
||||
|
||||
// Tabelle selbst: header sticky
|
||||
const thead = table.querySelector('thead');
|
||||
|
||||
|
||||
const headerCells = Array.from(table.querySelectorAll('thead th'));
|
||||
const getColumnIndex = name => headerCells.findIndex(c => c.textContent.trim() === name);
|
||||
|
||||
//------------------------------------
|
||||
// Dynamisches Filter-UI
|
||||
//------------------------------------
|
||||
function createDynamicFilterUI() {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.style="display:flex;flex-direction:row;align-items:center;gap:10px"
|
||||
|
||||
const colSelectLabel = document.createElement('label');
|
||||
const colSelect = document.createElement('select');
|
||||
const emptyOpt = document.createElement('option');
|
||||
emptyOpt.value = '';
|
||||
emptyOpt.textContent = '-- Alles --';
|
||||
colSelect.appendChild(emptyOpt);
|
||||
colSelectLabel.innerText = 'Spalte';
|
||||
|
||||
headerCells.forEach(cell => {
|
||||
if(!exceptedColumns.includes(cell.textContent.trim())) {
|
||||
const name = cell.textContent.trim();
|
||||
const opt = document.createElement('option');
|
||||
opt.value = name;
|
||||
opt.textContent = name;
|
||||
colSelect.appendChild(opt);
|
||||
}
|
||||
});
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'search';
|
||||
input.style.display = 'none';
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.style.display = 'none';
|
||||
select.innerHTML = '<option value="">-- Alle --</option>';
|
||||
|
||||
function populateDropdownForColumn(colName) {
|
||||
const colIndex = getColumnIndex(colName);
|
||||
const rows = Array.from(table.querySelectorAll('tbody tr')).filter(r => !r.classList.contains('grouprow'));
|
||||
const values = new Set();
|
||||
rows.forEach(r => {
|
||||
const cell = r.children[colIndex];
|
||||
if (cell) values.add(cell.textContent.trim());
|
||||
});
|
||||
const sorted = new Set([...values].sort((a, b) => a.localeCompare(b)));
|
||||
select.innerHTML = '<option value="">-- Alle --</option>' + [...sorted].map(v => `<option value="${v}">${v}</option>`).join('');
|
||||
}
|
||||
|
||||
colSelect.addEventListener('change', () => {
|
||||
state.selectedColumn = colSelect.value;
|
||||
state.searchText = '';
|
||||
state.dropdownValue = '';
|
||||
input.value = '';
|
||||
select.value = '';
|
||||
|
||||
if (!state.selectedColumn) {
|
||||
input.style.display = '';
|
||||
select.style.display = 'none';
|
||||
return applyFilters();
|
||||
}
|
||||
|
||||
const mode = cfg.columnModes[state.selectedColumn];
|
||||
if (mode === 'text') {
|
||||
input.style.display = '';
|
||||
select.style.display = 'none';
|
||||
} else if (mode === 'dropdown') {
|
||||
input.style.display = 'none';
|
||||
select.style.display = '';
|
||||
populateDropdownForColumn(state.selectedColumn);
|
||||
} else {
|
||||
input.style.display = '';
|
||||
select.style.display = 'none';
|
||||
}
|
||||
applyFilters();
|
||||
});
|
||||
input.style.display = '';
|
||||
|
||||
input.addEventListener('input', () => { state.searchText = input.value.toLowerCase(); applyFilters(); });
|
||||
select.addEventListener('change', () => { state.dropdownValue = select.value; applyFilters(); });
|
||||
|
||||
wrap.append(colSelectLabel, colSelect, document.createElement('br'));
|
||||
wrap.append(input, select);
|
||||
container.appendChild(wrap);
|
||||
}
|
||||
|
||||
//--------------------------------------------
|
||||
// Filterbreite an Parent minus Scrollbar
|
||||
//--------------------------------------------
|
||||
function updateFilterWidth() {
|
||||
if (!table || !container) return;
|
||||
|
||||
const wrapper = table.parentElement; // z. B. .table-wrapper
|
||||
if (!wrapper) return;
|
||||
|
||||
const style = getComputedStyle(table.querySelector('thead th'));
|
||||
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const scrollbarWidth = wrapper.offsetWidth - wrapper.clientWidth;
|
||||
|
||||
container.style.width = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px";
|
||||
container.style.maxWidth = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px";
|
||||
}
|
||||
|
||||
// Initial setzen
|
||||
updateFilterWidth();
|
||||
|
||||
// Fenstergröße beobachten
|
||||
window.addEventListener('resize', updateFilterWidth);
|
||||
|
||||
// Dynamische Anpassung bei Wrapper Resize
|
||||
const wrapper = table.parentElement;
|
||||
if (wrapper) {
|
||||
new ResizeObserver(updateFilterWidth).observe(wrapper);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------
|
||||
// Checkbox Filter
|
||||
//------------------------------------
|
||||
function createCheckboxFilter() {
|
||||
const cfgCheck = cfg.checkboxFilter;
|
||||
const idx = getColumnIndex(cfgCheck.column);
|
||||
if (idx < 0) return;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
|
||||
cfgCheck.rules.forEach(rule => {
|
||||
const checkWrapper = document.createElement('label');
|
||||
const checkInput = document.createElement('input');
|
||||
const checkTrack = document.createElement('span');
|
||||
const checkThumb = document.createElement('span');
|
||||
|
||||
checkWrapper.classList.add("cb", "cb-switch");
|
||||
checkWrapper.style.marginRight = '15px'
|
||||
checkInput.type = 'checkbox';
|
||||
checkInput.addEventListener('change', () => {
|
||||
if (checkInput.checked) state.checkbox.add(rule);
|
||||
else state.checkbox.delete(rule);
|
||||
applyFilters();
|
||||
});
|
||||
checkTrack.classList.add('switch-track');
|
||||
checkTrack.setAttribute("aria-hidden", 'true');
|
||||
|
||||
checkThumb.classList.add('switch-thumb');
|
||||
checkThumb.setAttribute("aria-hidden", 'true');
|
||||
|
||||
checkTrack.append(checkThumb)
|
||||
checkWrapper.append(rule.label, checkInput, checkTrack);
|
||||
wrapper.appendChild(checkWrapper);
|
||||
});
|
||||
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
//------------------------------------
|
||||
// Live Counter
|
||||
//------------------------------------
|
||||
const counter = document.createElement('div');
|
||||
counter.className = 'live-counter';
|
||||
container.appendChild(counter);
|
||||
|
||||
//------------------------------------
|
||||
// Filter Logik
|
||||
//------------------------------------
|
||||
function detectGroups() {
|
||||
if (groupsInitialized) return;
|
||||
groupsInitialized = true;
|
||||
|
||||
const rows = Array.from(table.querySelectorAll('tbody tr'));
|
||||
let currentGroupRow = null;
|
||||
let childBuffer = [];
|
||||
|
||||
rows.forEach(row => {
|
||||
if (row.classList.contains('grouprow')) {
|
||||
// vorige Gruppe speichern
|
||||
if (currentGroupRow && childBuffer.length > 0) {
|
||||
groupMap.set(currentGroupRow, childBuffer);
|
||||
}
|
||||
// neue Gruppe starten
|
||||
currentGroupRow = row;
|
||||
childBuffer = [];
|
||||
}
|
||||
else if (currentGroupRow) {
|
||||
childBuffer.push(row);
|
||||
}
|
||||
});
|
||||
|
||||
// letzte Gruppe speichern
|
||||
if (currentGroupRow && childBuffer.length > 0) {
|
||||
groupMap.set(currentGroupRow, childBuffer);
|
||||
}
|
||||
|
||||
// jedem groupRow ein Toggle verpassen (wenn nicht vorhanden)
|
||||
groupMap.forEach((childRows, groupRow) => {
|
||||
const toggle = groupRow.querySelector("span");
|
||||
|
||||
if (!toggle || toggle._groupToggleBound) return;
|
||||
toggle._groupToggleBound = true;
|
||||
|
||||
toggle.addEventListener("click", () => toggleGroup(groupRow));
|
||||
groupRow.addEventListener("dblclick", () => toggleGroup(groupRow));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function toggleGroup(groupRow) {
|
||||
const childRows = groupMap.get(groupRow);
|
||||
if (!childRows) return;
|
||||
|
||||
const toggle = groupRow.querySelector("span");
|
||||
const collapsed = toggle.textContent === '+';
|
||||
|
||||
if (collapsed) {
|
||||
// ausklappen → nur gefilterte Rows zeigen
|
||||
childRows.forEach(r => {
|
||||
if (!r._filteredOut) r.style.display = '';
|
||||
});
|
||||
toggle.textContent = '-';
|
||||
} else {
|
||||
// einklappen → alle verstecken
|
||||
childRows.forEach(r => r.style.display = 'none');
|
||||
toggle.textContent = '+';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function applyFilters() {
|
||||
detectGroups();
|
||||
const rows = Array.from(table.querySelectorAll('tbody tr'));
|
||||
let visible = 0;
|
||||
|
||||
rows.forEach(row => {
|
||||
if (row.classList.contains('grouprow')) { row.style.display = ''; return; }
|
||||
let show = true;
|
||||
|
||||
if (state.selectedColumn === '') {
|
||||
if (state.searchText) show = [...row.cells].some(c => c.textContent.toLowerCase().includes(state.searchText));
|
||||
} else {
|
||||
const idx = getColumnIndex(state.selectedColumn);
|
||||
const mode = cfg.columnModes[state.selectedColumn];
|
||||
if (mode === 'text' && state.searchText) {
|
||||
show = row.cells[idx]?.textContent.toLowerCase().includes(state.searchText);
|
||||
}
|
||||
if (mode === 'dropdown' && state.dropdownValue) {
|
||||
show = row.cells[idx]?.textContent === state.dropdownValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.checkbox.size > 0) {
|
||||
const idx = getColumnIndex(cfg.checkboxFilter.column);
|
||||
const cellVal = row.cells[idx]?.textContent ?? '';
|
||||
const pass = [...state.checkbox].some(rule => rule.test(cellVal));
|
||||
if (!pass) show = false;
|
||||
}
|
||||
|
||||
row._filteredOut = !show; // fürs Gruppensystem speichern
|
||||
|
||||
// Wenn Gruppen existieren:
|
||||
if (groupMap.size > 0) {
|
||||
// Prüfen ob row zu einer Gruppe gehört
|
||||
let parentGroup = null;
|
||||
for (const [g, list] of groupMap.entries()) {
|
||||
if (list.includes(row)) {
|
||||
parentGroup = g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (parentGroup) {
|
||||
const toggle = parentGroup.querySelector("span");
|
||||
const collapsed = toggle.textContent === '+';
|
||||
|
||||
if (collapsed) {
|
||||
// eingeklappt → immer ausblenden
|
||||
row.style.display = 'none';
|
||||
if (show) visible++;
|
||||
} else {
|
||||
// ausgeklappt → nur gefilterte anzeigen
|
||||
row.style.display = show ? '' : 'none';
|
||||
if (show) visible++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (show) visible++;
|
||||
}
|
||||
|
||||
// normale Zeile ohne Gruppe:
|
||||
row.style.display = show ? '' : 'none';
|
||||
});
|
||||
|
||||
counter.textContent = `${visible} Treffer`;
|
||||
}
|
||||
|
||||
//------------------------------------
|
||||
// Init
|
||||
//------------------------------------
|
||||
createDynamicFilterUI();
|
||||
if(options.filterConfig.checkboxFilter) createCheckboxFilter();
|
||||
applyFilters();
|
||||
|
||||
|
||||
|
||||
if (thead) {
|
||||
thead.style.position = 'sticky';
|
||||
thead.style.top = (container.offsetHeight - 1) + 'px';
|
||||
thead.style.zIndex = 10;
|
||||
}
|
||||
|
||||
//------------------------------------
|
||||
// Public API
|
||||
//------------------------------------
|
||||
return {
|
||||
refreshDropdown() { if (state.selectedColumn && cfg.columnModes[state.selectedColumn] === 'dropdown') {} },
|
||||
reapply() { applyFilters(); }
|
||||
};
|
||||
|
||||
|
||||
} catch(err) {
|
||||
alert(err.stack)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//#region Sort table
|
||||
// let sortDirection = {};
|
||||
function sortTable(tableId,colIndex) {
|
||||
const sortDirection = {};
|
||||
const table = document.getElementById(tableId);
|
||||
const tbody = table.tBodies[0];
|
||||
const rows = Array.from(tbody.querySelectorAll("tr"));
|
||||
|
||||
// Toggle sort direction
|
||||
sortDirection[colIndex] = !sortDirection[colIndex];
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const cellA = a.cells[colIndex].textContent.trim();
|
||||
const cellB = b.cells[colIndex].textContent.trim();
|
||||
|
||||
// Numeric sort if possible
|
||||
const numA = parseFloat(cellA);
|
||||
const numB = parseFloat(cellB);
|
||||
if (!isNaN(numA) && !isNaN(numB)) {
|
||||
return sortDirection[colIndex] ? numA - numB : numB - numA;
|
||||
} else {
|
||||
return sortDirection[colIndex]
|
||||
? cellA.localeCompare(cellB)
|
||||
: cellB.localeCompare(cellA);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove old rows
|
||||
tbody.innerHTML = "";
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
|
||||
// Update sort arrows
|
||||
const th = table.querySelectorAll("th");
|
||||
th.forEach((header, i) => {
|
||||
header.classList.remove("sort-asc", "sort-desc");
|
||||
if (i === colIndex) {
|
||||
header.classList.add(sortDirection[colIndex] ? "sort-asc" : "sort-desc");
|
||||
}
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
354
public/javascript/tutorial.js
Normal file
@@ -0,0 +1,354 @@
|
||||
class Tutorial {
|
||||
constructor(steps, options = {}) {
|
||||
this.steps = steps;
|
||||
this.currentStep = 0;
|
||||
this.isRunning = false;
|
||||
this.isWaitingForEvent = false;
|
||||
|
||||
this.options = {
|
||||
overlayOpacity: 0.7,
|
||||
...options
|
||||
};
|
||||
|
||||
this.zIndexBase = 1000;
|
||||
this.zIndexStep = 0;
|
||||
|
||||
this.modifiedElements = new Map();
|
||||
this.baseElement = null;
|
||||
this.currentElement = null;
|
||||
|
||||
this.tooltipObserver = null;
|
||||
this.tooltipRAF = null;
|
||||
this.currentStepElement = null;
|
||||
}
|
||||
|
||||
// ---------------- UI ----------------
|
||||
|
||||
createUI() {
|
||||
document.addEventListener("keydown", e => {
|
||||
if (this.isWaitingForEvent) return;
|
||||
|
||||
if (e.key === "ArrowRight") this.next();
|
||||
if (e.key === "ArrowLeft") this.prev();
|
||||
});
|
||||
|
||||
this.tooltip = document.createElement("div");
|
||||
this.tooltip.id = "tutorial-tooltip";
|
||||
|
||||
Object.assign(this.tooltip.style, {
|
||||
position: "absolute",
|
||||
display: "flex",
|
||||
padding: "12px",
|
||||
borderRadius: "8px",
|
||||
zIndex: 9999,
|
||||
maxWidth: "350px",
|
||||
boxShadow: "0 10px 30px rgba(0,0,0,0.2)",
|
||||
backdropFilter: "blur(5px)",
|
||||
justifyContent: "center"
|
||||
});
|
||||
|
||||
this.text = document.createElement("p");
|
||||
|
||||
this.nextBtn = document.createElement("button");
|
||||
this.nextBtn.innerHTML = ">";
|
||||
this.nextBtn.classList.add("bluebutton");
|
||||
|
||||
this.prevBtn = document.createElement("button");
|
||||
this.prevBtn.innerHTML = "<";
|
||||
this.prevBtn.classList.add("yellowbutton");
|
||||
|
||||
this.nextBtn.onclick = () => this.next();
|
||||
this.prevBtn.onclick = () => this.prev();
|
||||
|
||||
this.tooltip.appendChild(this.text);
|
||||
this.tooltip.appendChild(this.prevBtn);
|
||||
this.tooltip.appendChild(this.nextBtn);
|
||||
|
||||
document.body.appendChild(this.tooltip);
|
||||
}
|
||||
|
||||
// ---------------- Element wait ----------------
|
||||
|
||||
waitForElement(selector, timeout = 5000) {
|
||||
return new Promise(resolve => {
|
||||
const start = Date.now();
|
||||
|
||||
const check = () => document.querySelector(selector);
|
||||
|
||||
let el = check();
|
||||
if (el) return resolve(el);
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
el = check();
|
||||
if (el) {
|
||||
observer.disconnect();
|
||||
resolve(el);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (Date.now() - start > timeout) {
|
||||
clearInterval(timer);
|
||||
observer.disconnect();
|
||||
resolve(null);
|
||||
}
|
||||
|
||||
el = check();
|
||||
if (el) {
|
||||
clearInterval(timer);
|
||||
observer.disconnect();
|
||||
resolve(el);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------- Highlight ----------------
|
||||
|
||||
highlightElement(el) {
|
||||
this.removeHighlight();
|
||||
|
||||
if (!this.modifiedElements.has(el)) {
|
||||
this.modifiedElements.set(el, {
|
||||
zIndex: el.style.zIndex,
|
||||
position: el.style.position,
|
||||
boxShadow: el.style.boxShadow
|
||||
});
|
||||
}
|
||||
|
||||
if (this.currentStep === 0 && !this.baseElement) {
|
||||
this.baseElement = el;
|
||||
}
|
||||
|
||||
let zIndex;
|
||||
|
||||
if (el === this.baseElement) {
|
||||
zIndex = this.zIndexBase;
|
||||
} else {
|
||||
this.zIndexStep++;
|
||||
zIndex = this.zIndexBase + this.zIndexStep;
|
||||
}
|
||||
|
||||
el.style.position = "relative";
|
||||
el.style.zIndex = zIndex;
|
||||
el.style.boxShadow =
|
||||
"0 0 0 4px rgba(255, 0, 0, 1), 0 0 20px rgba(255,0,0,0.7)";
|
||||
|
||||
this.currentElement = el;
|
||||
|
||||
this.startTooltipTracking(el);
|
||||
}
|
||||
|
||||
removeHighlight() {
|
||||
if (!this.currentElement) return;
|
||||
|
||||
this.currentElement.style.boxShadow = "";
|
||||
this.stopTooltipTracking();
|
||||
}
|
||||
|
||||
// ---------------- Event system (NEW) ----------------
|
||||
|
||||
getEventConfig(type) {
|
||||
const map = {
|
||||
click: () => new MouseEvent("click", { bubbles: true, cancelable: true, view: window }),
|
||||
contextmenu: () => new MouseEvent("contextmenu", { bubbles: true, cancelable: true, view: window, button: 2 }),
|
||||
dblclick: () => new MouseEvent("dblclick", { bubbles: true, cancelable: true, view: window }),
|
||||
mouseenter: () => new MouseEvent("mouseenter", { bubbles: true, cancelable: true, view: window }),
|
||||
mouseover: () => new MouseEvent("mouseover", { bubbles: true, cancelable: true, view: window })
|
||||
};
|
||||
|
||||
return map[type] ? map[type]() : null;
|
||||
}
|
||||
|
||||
async executeStepOnly(step, el) {
|
||||
if (!step.action) return;
|
||||
|
||||
const event = this.getEventConfig(step.action);
|
||||
|
||||
if (event) {
|
||||
el.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
waitForUserEvent(el, type) {
|
||||
return new Promise(resolve => {
|
||||
const handler = (e) => {
|
||||
el.removeEventListener(type, handler);
|
||||
resolve(e);
|
||||
};
|
||||
|
||||
el.addEventListener(type, handler);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------- Tooltip tracking ----------------
|
||||
|
||||
startTooltipTracking(el) {
|
||||
this.stopTooltipTracking();
|
||||
|
||||
this.currentStepElement = el;
|
||||
|
||||
const update = () => {
|
||||
if (!this.currentStepElement || !this.tooltip) return;
|
||||
|
||||
const rect = this.currentStepElement.getBoundingClientRect();
|
||||
|
||||
this.tooltip.style.top = rect.bottom + 10 + "px";
|
||||
this.tooltip.style.left = rect.left + "px";
|
||||
|
||||
this.tooltipRAF = requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
this.tooltipObserver = new ResizeObserver(() => {
|
||||
if (!this.currentStepElement) return;
|
||||
|
||||
const rect = this.currentStepElement.getBoundingClientRect();
|
||||
|
||||
this.tooltip.style.top = rect.bottom + 10 + "px";
|
||||
this.tooltip.style.left = rect.left + "px";
|
||||
});
|
||||
|
||||
this.tooltipObserver.observe(el);
|
||||
|
||||
this.tooltipRAF = requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
stopTooltipTracking() {
|
||||
if (this.tooltipObserver) {
|
||||
this.tooltipObserver.disconnect();
|
||||
this.tooltipObserver = null;
|
||||
}
|
||||
|
||||
if (this.tooltipRAF) {
|
||||
cancelAnimationFrame(this.tooltipRAF);
|
||||
this.tooltipRAF = null;
|
||||
}
|
||||
|
||||
this.currentStepElement = null;
|
||||
}
|
||||
|
||||
// ---------------- Steps ----------------
|
||||
|
||||
async showStep() {
|
||||
let step = this.steps[this.currentStep];
|
||||
this.updateButtons(step);
|
||||
|
||||
|
||||
this.isWaitingForEvent = false;
|
||||
|
||||
while (step && (!step.text || step.text.trim() === "")) {
|
||||
const elSilent = await this.waitForElement(step.element);
|
||||
|
||||
if (elSilent) {
|
||||
await this.executeStepOnly(step, elSilent);
|
||||
}
|
||||
|
||||
this.currentStep++;
|
||||
|
||||
if (this.currentStep >= this.steps.length) {
|
||||
this.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
step = this.steps[this.currentStep];
|
||||
}
|
||||
|
||||
const el = await this.waitForElement(step.element);
|
||||
|
||||
if (!el) {
|
||||
this.next();
|
||||
return;
|
||||
}
|
||||
|
||||
this.highlightElement(el);
|
||||
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
this.tooltip.style.display = "block";
|
||||
this.text.innerHTML = step.text;
|
||||
|
||||
this.tooltip.style.top = rect.bottom + 10 + "px";
|
||||
this.tooltip.style.left = rect.left + "px";
|
||||
|
||||
this.isWaitingForEvent = !!step.waitFor;
|
||||
|
||||
this.nextBtn.style.display = step.waitFor ? "none" : "inline-block";
|
||||
this.prevBtn.style.display =
|
||||
this.currentStep === 0 || step.waitFor ? "none" : "inline-block";
|
||||
|
||||
if (step.waitFor) {
|
||||
await this.waitForUserEvent(el, step.waitFor);
|
||||
this.isWaitingForEvent = false;
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
|
||||
updateButtons(step) {
|
||||
const isLastStep = this.currentStep === this.steps.length - 1;
|
||||
|
||||
this.nextBtn.innerHTML = isLastStep ? "Fertig" : ">";
|
||||
}
|
||||
|
||||
|
||||
// ---------------- Navigation ----------------
|
||||
|
||||
start() {
|
||||
if (this.isRunning) return;
|
||||
|
||||
this.isRunning = true;
|
||||
this.createUI();
|
||||
this.currentStep = 0;
|
||||
this.showStep();
|
||||
}
|
||||
|
||||
next() {
|
||||
if (this.isWaitingForEvent) return;
|
||||
|
||||
this.removeHighlight();
|
||||
|
||||
if (this.currentStep < this.steps.length - 1) {
|
||||
this.currentStep++;
|
||||
this.showStep();
|
||||
} else {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
prev() {
|
||||
if (this.isWaitingForEvent) return;
|
||||
|
||||
this.removeHighlight();
|
||||
|
||||
if (this.currentStep > 0) {
|
||||
this.currentStep--;
|
||||
this.showStep();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- Cleanup ----------------
|
||||
|
||||
destroy() {
|
||||
this.isRunning = false;
|
||||
|
||||
this.stopTooltipTracking();
|
||||
|
||||
this.modifiedElements.forEach((styles, el) => {
|
||||
el.style.zIndex = styles.zIndex || "";
|
||||
el.style.position = styles.position || "";
|
||||
el.style.boxShadow = styles.boxShadow || "";
|
||||
});
|
||||
|
||||
this.modifiedElements.clear();
|
||||
this.baseElement = null;
|
||||
this.zIndexStep = 0;
|
||||
|
||||
this.tooltip?.remove();
|
||||
}
|
||||
}
|
||||
131
public/styles/colors.css
Normal file
@@ -0,0 +1,131 @@
|
||||
/* #region OS */
|
||||
#desktop { background:var(--theme-desktop-backcolor); background-image:var(--theme-desktop-image); }
|
||||
#taskbar { background:var(--theme-taskbar-backcolor); color:var(--theme-taskbar-color); }
|
||||
#tutorial-tooltip {background:var(--theme-window-backcolor); color:var(--theme-window-color);}
|
||||
#tutorial-tooltip img {filter: invert(1);}
|
||||
|
||||
#start-btn { background:var(--theme-accent-default-backcolor); color:var(--theme-accent-default-color); }
|
||||
#start-btn:hover { background:var(--theme-accent-hover-backcolor); color:var(--theme-accent-hover-color); }
|
||||
#start-btn:active { background:var(--theme-accent-active-backcolor); color:var(--theme-accent-active-color); }
|
||||
|
||||
/* #start-menu, .submenu { background:var(--theme-taskbar-backcolor); color:var(--theme-taskbar-color); } */
|
||||
#start-menu { padding-bottom:10px; background:var(--theme-startmenu-backcolor); color:var(--theme-startmenu-color); border:3px solid rgb(128,128,128); }
|
||||
.start-submenu-head { background:var(--theme-startmenu-submenu-header-backcolor); color:var(--theme-startmenu-submenu-header-backcolor) }
|
||||
.start-icon { background:var(--theme-accent-default-backcolor); }
|
||||
|
||||
/* .start-item:hover, .start-sys-item:hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); } */
|
||||
.start-item.has-submenu.open > .menu-label , .start-item:not(.has-submenu.open), .start-sys-item { background-color:var(--theme-startmenu-item-default-backcolor); color:var(--theme-startmenu-item-default-color); }
|
||||
.start-item:not(.has-submenu.open):not(.unload):hover, .start-sys-item:hover { background-color:var(--theme-startmenu-item-hover-backcolor); color:var(--theme-startmenu-item-hover-color); }
|
||||
.start-item:not(.has-submenu):not(.has-submenu.open):not(.unload):active, .start-sys-item:active { color:var(--theme-accent-active-color); background:var(--theme-accent-active-backcolor); }
|
||||
.start-item-sys-container { background:var(--theme-startmenu-syscontainer-backcolor);}
|
||||
.start-item .unload { background:var(--theme-startmenu-item-disabled-backcolor); color:var(--theme-startmenu-item-disabled-color); }
|
||||
/* #taskbar .taskbar-item { background:var(--theme-taskbar-item-backcolor); } */
|
||||
|
||||
.taskbar-item { position: relative;}
|
||||
.taskbar-item::before { background: var(--theme-accent-active-color); }
|
||||
.taskbar-item.focus::before { background: var(--theme-accent-active-backcolor); }
|
||||
/* .taskbar-item.minimized { background:var(--theme-taskbar-item-minimized-backcolor); color:var(--theme-taskbar-item-minimized-color); border-color:var(--theme-taskbar-item-minimized-border-color);} */
|
||||
.taskbar-item.default { background:var(--theme-taskbar-item-default-backcolor); color:var(--theme-taskbar-item-default-color); border-color:var(--theme-taskbar-item-default-border-color);}
|
||||
.taskbar-item:hover { background-color:var(--theme-startmenu-item-hover-backcolor); color:var(--theme-startmenu-item-hover-color); }
|
||||
|
||||
.window { border:2px solid var(--theme-window-titlebar-backcolor); background:var(--theme-window-backcolor); }
|
||||
.window-content { color:var(--theme-window-color); background:var(--theme-window-backcolor); }
|
||||
.window-titlebar { color:var(--theme-window-titlebar-color); background:var(--theme-window-titlebar-backcolor); }
|
||||
.window .controls button { color:var(--theme-window-titlebar-color); background:transparent; }
|
||||
.window .controls button:hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Table */
|
||||
table, tr, td, th { border-color:var(--theme-table-border-color); }
|
||||
table thead th { background-color:var(--theme-table-header-backcolor); color:var(--theme-table-header-color); border-color:var(--theme-table-border-color); }
|
||||
table tbody tr:nth-child(even) { background-color:var(--theme-table-rows-even-backcolor); color:var(--theme-table-rows-even-color); }
|
||||
table tbody tr:nth-child(odd) { background-color:var(--theme-table-rows-odd-backcolor); color:var(--theme-table-rows-odd-color); }
|
||||
table tbody tr:not(.grouprow):not(.no-hover):hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); }
|
||||
table tbody tr.grouprow { background-color:var(--theme-table-rows-grouprow-backcolor); color:var(--theme-table-rows-grouprow-color); }
|
||||
table tbody tr.active, table tbody td.active { color:var(--theme-accent-active-color); background:var(--theme-accent-active-backcolor); }
|
||||
.table-filter-container { border-color:var(--theme-table-border-color); background-color:var(--theme-table-header-backcolor); color:var(--theme-table-header-color); }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Scrollbar */
|
||||
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background:var(--theme-scrollbar-backcolor); }
|
||||
::-webkit-scrollbar-thumb { background-color:var(--theme-scrollbar-thumb-default-backcolor); border-color:var(--theme-scrollbar-thumb-default-border-color); }
|
||||
::-webkit-scrollbar-thumb:hover { background-color:var(--theme-scrollbar-thumb-hover-backcolor); border-color:var(--theme-scrollbar-thumb-hover-border-color); }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Tooltip */
|
||||
.global-tooltip { background:var(--theme-tooltip-backcolor); color:var(--theme-tooltip-color); border-color:var(--theme-tooltip-border-color) }
|
||||
/* #endregion */
|
||||
|
||||
/* #region feedbox */
|
||||
.feedbox { background:var(--theme-feedbox-backcolor); color:var(--theme-feedbox-color);box-shadow:0 5px 25px rgba(0,0,0,0.3); }
|
||||
.feedbox-overlay { background:rgba(0,0,0,0.5); }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Messagbox */
|
||||
.test, .test .countdown { color:var(--theme-message-test-color); background-color:var(--theme-message-test-backcolor); border-color:var(--theme-message-test-border-color); }
|
||||
.success, .success .countdown { color:var(--theme-message-success-color); background-color:var(--theme-message-success-backcolor); border-color:var(--theme-message-success-border-color); }
|
||||
.error, .error .countdown { color:var(--theme-message-error-color); background:var(--theme-message-error-backcolor); border-color:var(--theme-message-error-border-color); }
|
||||
.info, .info .countdown { color:var(--theme-message-info-color); background-color:var(--theme-message-info-backcolor); border-color:var(--theme-message-info-border-color); }
|
||||
.warn, .warn .countdown { color:var(--theme-message-warn-color); background-color:var(--theme-message-warn-backcolor); border-color:var(--theme-message-warn-border-color); }
|
||||
.throw_exception, .throw_exception .countdown { color:var(--theme-message-throw-color); background-color:var(--theme-message-throw-backcolor); border-color:var(--theme-message-throw-border-color); }
|
||||
.message { box-shadow:0 4px 10px rgba(0,0,0,0.3); } /* SHADOW? */
|
||||
/* #endregion */
|
||||
|
||||
.error-field { background:var(--theme-message-error-backcolor); border-color:var(--theme-message-error-border-color); color:var(--theme-message-error-color); }
|
||||
.success-field { background:var(--theme-message-success-backcolor); border-color: var(--theme-message-success-border-color); color:var(--theme-message-success-color); }
|
||||
|
||||
/* region Container*/
|
||||
.card { background:var(--theme-container-card-backcolor); border-color:var(--theme-container-card-border); }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Checkbox */
|
||||
.cb-box { border-color:var(--theme-checkbox-default-border-color); box-shadow:0 1px 2px var(--theme-checkbox-shadow-color) inset; }
|
||||
.cb input:checked + .cb-box { background:var(--theme-checkbox-default-thumb); border-color:var(--theme-checkbox-default-thumb); }
|
||||
.cb input:disabled + .cb-box { background:var(--theme-checkbox-disabled-backcolor); border-color:var(--theme-checkbox-disabled-border-color); }
|
||||
.cb-label { color:var(--theme-checkbox-color); }
|
||||
/* #endregion */
|
||||
|
||||
/* #region Switch */
|
||||
.cb-switch .cb-label { color:var(--theme-switch-color); }
|
||||
.switch-track { background:var(--theme-switch-backcolor); border-color:var(--theme-switch-border-color); }
|
||||
/* #endregion */
|
||||
|
||||
/* #region Required */
|
||||
.is-required-empty { border-width:2px; border-color:var(--theme-required-empty); border-style:solid; }
|
||||
.is-required-filled { border-color:var(--theme-required-accept); }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Tabs */
|
||||
.tabs { border-bottom-color: #ccc; }
|
||||
.tab-content { border-color: #ccc; }
|
||||
.tab:hover { background-color: var(--theme-accent-hover-backcolor); color: var(--theme-accent-hover-color); }
|
||||
.tab.active { background-color: var(--theme-accent-active-backcolor); color: var(--theme-accent-active-color); border-color: var(--theme-accent-active-border-color); border-bottom-color: var(--theme-accent-active-backcolor); }
|
||||
/* #endregion */
|
||||
|
||||
/* #region DOM */
|
||||
button:not(:disabled).monolyth { background-color:var(--theme-button-monolyth-default-backcolor); color:var(--theme-button-monolyth-default-color); }
|
||||
button:not(:disabled).monolyth:hover { background-color:var(--theme-button-monolyth-hover-backcolor); color:var(--theme-button-monolyth-hover-color); }
|
||||
button:not(:disabled).monolyth:hover, button:not(:disabled).bluebutton:hover, button:not(:disabled).redbutton:hover, button:not(:disabled).yellowbutton:hover, button:not(:disabled).greenbutton:hover { box-shadow:0 6px 8px rgba(0,0,0,0.15); }
|
||||
|
||||
select { background:var(--theme-select-default-backcolor); color:var(--theme-select-default-color); box-shadow:0 0 5px var(--theme-select-default-border-color); }
|
||||
input:hover, input[type="text"]:hover, input[type="email"]:hover, input[type="password"]:hover, input[type="number"]:hover, input[type="date"]:hover,textarea:hover, select:hover { background:var(--theme-input-hover-backcolor); color:var(--theme-input-hover-color); box-shadow:0 0 5px var(--theme-input-hover-border-color); }
|
||||
input:focus, input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus, input[type="number"]:focus, input[type="date"]:focus, textarea:focus, select:focus { background:var(--theme-input-focus-backcolor); color:var(--theme-input-focus-color); box-shadow:0 0 5px var(--theme-input-focus-border-color); }
|
||||
input.valid { border: 2px solid var(--theme-input-accept-border-color); }
|
||||
input.invalid { border: 2px solid var(--theme-input-decline-border-color); }
|
||||
|
||||
*::placeholder, input[id="sAMAccountName"] { color:var(--theme-input-placeholder-color); }
|
||||
|
||||
textarea, input { border-color:var(--theme-input-default-border-color); background:var(--theme-input-default-backcolor); color:var(--theme-input-default-color); }
|
||||
textarea:not([required]):hover, input:not([required]):hover { border-color:var(--theme-input-hover-border-color); background-color:var(--theme-input-hover-backcolor); color:var(--theme-input-hover-color); }
|
||||
textarea:focus, input:focus { border-color:var(--theme-input-focus-border-color); background-color:var(--theme-input-focus-backcolor); color:var(--theme-input-focus-color); }
|
||||
|
||||
select, select option { border-color:var(--theme-select-border-color); background-color:var(--theme-select-backcolor); color:var(--theme-select-color); }
|
||||
select:focus, select:not([required]):hover, select:focus option,select:not([required]):hover option { border-color:var(--theme-select-hover-border-color); background-color:var(--theme-select-hover-backcolor); color:var(--theme-select-hover-color); }
|
||||
|
||||
/* #endregion */
|
||||
79
public/styles/contextMenu.css
Normal file
@@ -0,0 +1,79 @@
|
||||
.ctx-menu {
|
||||
position: fixed;
|
||||
min-width: 180px;
|
||||
background: var(--theme-contextmenu-backcolor);
|
||||
border: 1px solid var(--theme-contextmenu-border-color);
|
||||
color: var(--theme-contextmenu-color);
|
||||
border-radius: 6px;
|
||||
padding: 4px 0;
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.ctx-menu.hidden {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ctx-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ctx-item {
|
||||
padding: 5px 7px 5px 20px;
|
||||
margin-left: 1px;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ctx-item:hover {
|
||||
background: var(--theme-contextmenu-hover-backcolor);
|
||||
color: var(--theme-contextmenu-hover-color);
|
||||
}
|
||||
|
||||
|
||||
.ctx-item .icon {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
justify-content: flex-start;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
|
||||
.ctx-disabled {
|
||||
opacity: 0.5;
|
||||
/* cursor: default; */
|
||||
}
|
||||
|
||||
.ctx-divider {
|
||||
border-top: 1px solid var(--theme-contextmenu-divider);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.ctx-submenu {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
min-width: 160px;
|
||||
background: var(--theme-contextmenu-backcolor);
|
||||
border: 1px solid var(--theme-contextmenu-border-color);
|
||||
color: var(--theme-contextmenu-color);
|
||||
padding: 4px 0;
|
||||
border-radius: 6px;
|
||||
display: none;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.ctx-submenu.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ctx-arrow {
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
434
public/styles/default.css
Normal file
@@ -0,0 +1,434 @@
|
||||
/* #region DOM */
|
||||
*::placeholder { text-align:center; }
|
||||
* {
|
||||
user-select:none;
|
||||
cursor: var(--theme-cursor-default) 1 1, auto;
|
||||
}
|
||||
|
||||
|
||||
.selectable { user-select: text !important; cursor: var(--theme-cursor-pointer) 0 16, pointer; }
|
||||
|
||||
input, input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="date"], textarea, select { border-width:2px; border-style:solid; font-size: var(--fontSize); font-family: var(--fontFamily); border-radius:10px; padding:10px 12px; outline:none; transition:all .5s ease; width:auto; }
|
||||
input.width\:100px { width:100px; }
|
||||
input.width\:90px { width:90px; }
|
||||
input.width\:75px { width:75px; }
|
||||
input.width\:50px { width:50px; }
|
||||
input.width\:25px { width:25px; }
|
||||
|
||||
*::placeholder, input[id="sAMAccountName"] { font-style:italic; font-weight:100; letter-spacing:3px; }
|
||||
|
||||
html, button { font-size: var(--fontSize); font-family: var(--fontFamily); }
|
||||
button.monolyth, button.bluebutton, button.greenbutton, button.yellowbutton, button.redbutton { display:inline-block; padding:8px 10px; margin:0.2rem 1.6rem; font-weight:600; text-align:center; text-decoration:none; color:rgb(255, 255, 255); border:none; border-radius:8px; box-shadow:0 4px 6px rgba(0,0,0,0.1); transition:all 0.2s ease; }
|
||||
button.monolyth { background-color:transparent; }
|
||||
button:not(:disabled).monolyth:hover { opacity:0.9; }
|
||||
button.bluebutton { color:var(--theme-button-blue-default-color); background:var(--theme-button-blue-default-backcolor); }
|
||||
button.greenbutton { color:var(--theme-button-green-default-color); background:var(--theme-button-green-default-backcolor); }
|
||||
button.yellowbutton { color:var(--theme-button-yellow-default-color); background:var(--theme-button-yellow-default-backcolor); }
|
||||
button.redbutton { color:var(--theme-button-red-default-color); background:var(--theme-button-red-default-backcolor); }
|
||||
button:disabled { color:var(--theme-button-disabled-color); background:var(--theme-button-disabled-backcolor); }
|
||||
button:not(:disabled).redbutton:hover { background:var(--theme-button-red-hover-color); background:var(--theme-button-red-hover-backcolor); }
|
||||
button:not(:disabled).greenbutton:hover { background:var(--theme-button-green-hover-color); background:var(--theme-button-green-hover-backcolor); }
|
||||
button:not(:disabled).bluebutton:hover { background:var(--theme-button-blue-hover-color); background:var(--theme-button-blue-hover-backcolor); }
|
||||
button:not(:disabled).yellowbutton:hover { background:var(--theme-button-yellow-hover-color); background:var(--theme-button-yellow-hover-backcolor); }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
|
||||
/* #region Container */
|
||||
.container.static { width:calc(100% - 20px); margin:10px auto; display:flex; gap:12px; min-height:0; overflow:auto; max-height:100%; flex-direction: column;}
|
||||
/* .card.static { display:flex; flex-direction:column;flex: 0 0 auto; } */
|
||||
.card.static.row { overflow:hidden; display:flex; flex-direction:row; flex-wrap: wrap;}
|
||||
.card.static { overflow:hidden; display:flex; flex-direction:column; }
|
||||
|
||||
.container { width:calc(100% - 20px); margin:10px auto; display:grid; grid-template-columns:100%; gap:12px; min-height:0; overflow:auto; max-height:100%; }
|
||||
.container:not(.static) * { box-sizing:border-box; }
|
||||
.card { border-width:1px; border-style:solid; border-radius:8px; padding:20px; }
|
||||
|
||||
.grid { display:grid; gap:16px; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Copy icon */
|
||||
.copy-icon { transition:opacity .25s; cursor:var(--theme-cursor-pointer) -16 16, pointer; opacity:0.7; }
|
||||
.copy-icon:hover { opacity:1; }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Tooltip */
|
||||
.global-tooltip { position:fixed; padding:6px 8px; border-width:1px; border-style:solid; border-radius:10px; max-width:50vw; white-space:normal; z-index:999; pointer-events:none; opacity:0; transform:translateY(-30px); transition:opacity .12s ease; }
|
||||
.global-tooltip.visible { opacity:1; }
|
||||
/* #endregion*/
|
||||
|
||||
|
||||
/* #region Scrollbar */
|
||||
::-webkit-scrollbar { width:12px; height:12px; }
|
||||
::-webkit-scrollbar-track { border-radius:0px; }
|
||||
::-webkit-scrollbar-thumb { border-radius:6px; border-width:2px; border-style:solid; }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Messagebox */
|
||||
#message-container { position: fixed; top: 1rem; right: 1px; display: flex; flex-direction: column; gap: 0.5rem; z-index: 1000; max-height: 80vh; padding-left: 15px; overflow-y: auto; overflow-x: visible; scrollbar-width: none; -ms-overflow-style: none; }
|
||||
#message-container::-webkit-scrollbar { display: none; }
|
||||
.message { border-radius: 8px; margin: 8px 8px 8px 0; padding: 10px 14px; width: auto; max-width: 45vw; animation: slideIn 0.4s ease-out; transition: transform 0.2s ease; word-break: break-word; overflow-wrap: anywhere; }
|
||||
.message:hover { transform:scale(1.02); }
|
||||
.message-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; }
|
||||
.message-title { flex:1; font-weight:600; }
|
||||
.countdown { margin-right:8px; white-space:nowrap; }
|
||||
.pin-div { margin-left:8px; cursor:var(--theme-cursor-pointer) -16 16, pointer; user-select:none; transition:transform 0.2s ease, color 0.2s ease; }
|
||||
.pin-div:hover { transform:scale(1.2); }
|
||||
.pin-div.pinned { transform:scale(1.1); }
|
||||
@keyframes slideIn { 0% { opacity:0; transform:translateX(100%); } 60% { opacity:1; transform:translateX(-10px); } 80% { opacity:1; transform:translateX(5px); } 100% { opacity:1; transform:translateX(-10px); } }
|
||||
@keyframes slideOut { 0% { opacity:1; transform:translateX(0); } 100% { opacity:1; transform:translateX(100%); } }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Toggle switch */
|
||||
/*
|
||||
<label class="cb cb-switch">
|
||||
<input data-status="{{name}}" type="checkbox" {{#if active}}checked{{/if}} >
|
||||
<span class="switch-track" aria-hidden="true">
|
||||
<span class="switch-thumb" aria-hidden="true"></span>
|
||||
</span>
|
||||
<span class="cb-label">Ein / Aus</span>
|
||||
</label>
|
||||
*/
|
||||
.cb-switch { --w:45px; --h:27px; display:inline-flex; align-items:center; }
|
||||
.cb-switch input { position:absolute; opacity:0; width:0; height:0; pointer-events:none; }
|
||||
.switch-track { width:var(--w); height:var(--h); border-radius:999px; padding:3px; border-width:1px; border-style:solid; box-sizing:border-box; display:inline-flex; align-items:center; transition:background .18s ease, transform .12s ease, border-color .25s ease; }
|
||||
.switch-thumb { min-width:calc(var(--h) - 2 * 3px); width:calc(var(--h) - 2 * 3px); height:calc(var(--h) - 2 * 3px); background:var(--theme-switch-thumb); border-radius:50%; box-shadow:0 2px 6px rgba(0,0,0,.15); transform:translateX(-1px); transition:transform .25s cubic-bezier(.2,.9,.2,1), background .18s ease }
|
||||
|
||||
|
||||
|
||||
.cb-switch input:disabled + .switch-track { background-color: dimgray;}
|
||||
.cb-switch input:disabled + .switch-track .switch-thumb { background-color: rgb(54, 50, 50); }
|
||||
|
||||
.cb-switch input:not(:disabled):checked + .switch-track { background:var(--theme-switch-active); }
|
||||
|
||||
.cb-switch input:not(:disabled):hover + .switch-track { border-color:var(--theme-switch-hover); }
|
||||
|
||||
.cb-switch input:focus-visible + .switch-track { box-shadow:0 0 0 6px rgba(6,193,103,0.12); }
|
||||
.cb-switch input:checked + .switch-track .switch-thumb { transform:translateX(calc(var(--w) - var(--h))); }
|
||||
.cb-switch label { width: calc(100% - var(--w)); }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region CheckBox */
|
||||
/*
|
||||
<label class="cb cb-modern">
|
||||
<input data-status="{{name}}" type="checkbox" {{#if active}}checked{{/if}} disabled>
|
||||
<span class="cb-box" aria-hidden="true"></span>
|
||||
</label>
|
||||
*/
|
||||
.cb { display:inline-flex; align-items:center; gap:10px; user-select:none; transform:translateY(2px); }
|
||||
.cb input { position:absolute; opacity:0; width:0; height:0; pointer-events:none; }
|
||||
.cb-box { width:20px; height:20px; border-radius:6px; background:var(--theme-checkbox-backcolor); border-width:2px; border-style:solid; display:inline-grid; place-items:center; transition:transform .12s ease, border-color .12s ease, background .12s ease; }
|
||||
.cb-box::after { content:""; width:12px; height:8px; transform:scale(0) translateY(-2px); background-repeat:no-repeat; background-position:center; background-size:contain; background-image:url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 4L4 7l7-7' stroke='%23fff' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); transition:transform .18s cubic-bezier(.2,.9,.2,1); }
|
||||
.cb input:checked + .cb-box { transform:translateY(-1px); }
|
||||
.cb input:checked + .cb-box::after { transform:scale(1) translate(-1px, 1px); }
|
||||
table .cb input:checked + .cb-box::after { transform:scale(1) translate(1px, 1px); }
|
||||
.cb input:focus-visible + .cb-box { outline:none; }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Required */
|
||||
.is-required-empty { border-width:2px; border-style:solid; }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Tabs */
|
||||
.tabs { display: flex; margin-bottom: 10px; border-bottom-width: 2px; border-bottom-style: solid; }
|
||||
.tab { padding: 10px 20px; border: 1px solid transparent; border-top-left-radius: 5px; border-top-right-radius: 5px; margin-right: 5px; transition: background .25s, color .25s, border-color .25s; }
|
||||
.tab-content { border-width: 1px; border-style: solid; border-radius: 5px; padding: 15px; }
|
||||
.item { margin-bottom: 5px; }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region Feebox */
|
||||
.feedbox-overlay { position:fixed; top:0; left:0; width:100vw; height:100vh; display:flex; align-items:center; justify-content:center; z-index:999; }
|
||||
.feedbox { border-radius:8px; max-width:50vw; width:100%; padding:20px; animation:feedboxFadeIn 0.2s ease-out; max-height:80vh; display:flex; flex-direction:column; overflow:hidden; }
|
||||
.feedbox h3 { margin:0 0 10px 0; }
|
||||
.feedbox-message { margin-bottom:20px; line-height:1.4; flex:1; overflow-y: auto; /* font-size:1rem; */ }
|
||||
.feedbox-actions { display:flex; justify-content:flex-end; gap:10px; flex-wrap:wrap; flex-shrink:0; }
|
||||
@keyframes feedboxFadeIn { from { transform:translateY(-20px); opacity:0; } to { transform:translateY(0); opacity:1; } }
|
||||
|
||||
.feedbox-btn {
|
||||
padding:6px 14px;
|
||||
/* font-size:0.95rem; */
|
||||
border-radius:4px;
|
||||
border:1px solid #ccc;
|
||||
background:#f5f5f5;
|
||||
transition:all 0.15s;
|
||||
}
|
||||
|
||||
.feedbox-btn:hover {
|
||||
background:#e5e5e5;
|
||||
}
|
||||
|
||||
.feedbox-btn.primary {
|
||||
background:#4a90e2;
|
||||
color:#fff;
|
||||
border-color:#4a90e2;
|
||||
}
|
||||
|
||||
.feedbox-btn.primary:hover {
|
||||
background:#357ABD;
|
||||
}
|
||||
|
||||
.feedbox-btn.danger {
|
||||
background:#e94e3c;
|
||||
color:#fff;
|
||||
border-color:#e94e3c;
|
||||
}
|
||||
|
||||
.feedbox-btn.danger:hover {
|
||||
background:#c0392b;
|
||||
}
|
||||
|
||||
.feedbox-input {
|
||||
width:100%;
|
||||
padding:6px 8px;
|
||||
/* font-size:0.95rem; */
|
||||
margin-top:8px;
|
||||
margin-bottom:12px;
|
||||
border:1px solid #ccc;
|
||||
border-radius:4px;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.feedbox-btn:focus, .feedbox-input:focus {
|
||||
outline:2px solid #4a90e2;
|
||||
outline-offset:2px;
|
||||
}
|
||||
|
||||
/* Responsive Small Screens */
|
||||
@media (max-width:400px) {
|
||||
.feedbox {
|
||||
padding:15px;
|
||||
}
|
||||
|
||||
.feedbox-actions {
|
||||
flex-direction:column-reverse;
|
||||
gap:8px;
|
||||
}
|
||||
|
||||
.feedbox-btn {
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.select-wrapper select {
|
||||
width: 100%;
|
||||
padding: 12px 40px 12px 14px;
|
||||
font-size: 16px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
appearance: none; /* entfernt Standard-Styling */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Hover / Focus */
|
||||
.select-wrapper select:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.select-wrapper select:focus {
|
||||
outline: none;
|
||||
border-color: #4a90e2;
|
||||
box-shadow: 0 0 0 3px rgba(74,144,226,0.2);
|
||||
}
|
||||
|
||||
/* Custom Pfeil */
|
||||
.select-wrapper::after {
|
||||
content: "▾";
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.error-field { padding:12px 16px; border-radius:10px; border-width:1px; border-style:solid; }
|
||||
.success-field { border-width: 1px; border-style: solid; padding:12px 16px; border-radius:10px; }
|
||||
|
||||
label { color:var(--muted); display:block; margin-bottom:6px; }
|
||||
|
||||
|
||||
/* #region Dropzone */
|
||||
.dropzone {
|
||||
max-width:360px;
|
||||
padding:12px;
|
||||
box-shadow:0 6px 18px rgba(0,0,0,.08);
|
||||
|
||||
background:var(--theme-dropzone-default-backcolor);
|
||||
|
||||
border:1px dashed #aaa;
|
||||
border-radius:8px;
|
||||
padding:10px;
|
||||
/* font-size:13px; */
|
||||
text-align:center;
|
||||
margin-bottom:8px;
|
||||
color:var(--theme-dropzone-default-color);
|
||||
}
|
||||
|
||||
.dropzone.active {
|
||||
border-color:var(--theme-dropzone-active-border-color);
|
||||
color:var(--theme-dropzone-active-color);
|
||||
background:var(--theme-dropzone-active-backcolor);
|
||||
}
|
||||
|
||||
|
||||
.dropzone-area ul {
|
||||
list-style:none;
|
||||
padding:0;
|
||||
margin:0;
|
||||
max-height:120px;
|
||||
overflow-y:auto;
|
||||
}
|
||||
|
||||
.dropzone-area li {
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
/* font-size:13px; */
|
||||
padding:4px 0;
|
||||
border-bottom:1px solid #eee;
|
||||
}
|
||||
|
||||
button.removeButton {
|
||||
border:none;
|
||||
background:none;
|
||||
color:#d11a2a;
|
||||
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||
/* font-size:14px; */
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display:none;
|
||||
}
|
||||
|
||||
tr.drop-hover {
|
||||
outline:3px dashed var(--theme-dropzone-active-border-color);
|
||||
/* outline:2px dashed var(--primary-color, #4a90e2);
|
||||
background:rgba(74, 144, 226, 0.08); */
|
||||
}
|
||||
|
||||
tr.no-drop-hover {
|
||||
outline:3px dashed red;
|
||||
cursor:no-drop !important;
|
||||
/* outline:2px dashed var(--primary-color, #4a90e2);
|
||||
background:rgba(74, 144, 226, 0.08); */
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* #region Multiselect textbox */
|
||||
.mst-wrapper {
|
||||
position:relative;
|
||||
display:block;
|
||||
border-radius:4px;
|
||||
padding:4px 8px;
|
||||
background:var(--theme-window-backcolor);
|
||||
|
||||
cursor:text;
|
||||
width:300px; /* optional, je nach gewünschter Breite */
|
||||
}
|
||||
|
||||
/* Eingabefeld oben */
|
||||
.mst-input {
|
||||
width:100%;
|
||||
border:none;
|
||||
outline:none;
|
||||
/* font-size:14px; */
|
||||
padding:4px 0;
|
||||
}
|
||||
|
||||
/* Container für Chips unterhalb des Inputs */
|
||||
.mst-chips-container {
|
||||
display:flex;
|
||||
flex-wrap:wrap; /* Chips umbrechen */
|
||||
gap:4px;
|
||||
margin-top:4px;
|
||||
}
|
||||
|
||||
/* Einzelner Chip */
|
||||
.mst-chip {
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
background-color:#007bff;
|
||||
color:#fff;
|
||||
border-radius:8px;
|
||||
padding:8px 8px;
|
||||
/* font-size:13px; */
|
||||
}
|
||||
|
||||
/* Entfernen-Button im Chip */
|
||||
.mst-chip-remove {
|
||||
margin-left:4px;
|
||||
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
/* Dropdown-Liste */
|
||||
.mst-dropdown {
|
||||
position:absolute;
|
||||
top:100%;
|
||||
left:0;
|
||||
right:0;
|
||||
max-height:200px;
|
||||
overflow-y:auto;
|
||||
border:1px solid #ccc;
|
||||
background:inherit;
|
||||
z-index:1000;
|
||||
}
|
||||
|
||||
.mst-item {
|
||||
padding:6px 8px;
|
||||
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||
}
|
||||
|
||||
.mst-item:hover {
|
||||
background-color:var(--theme-accent-hover-backcolor);
|
||||
}
|
||||
|
||||
.mst-item.new {
|
||||
font-style:italic;
|
||||
color:#555;
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
|
||||
.tutorial-highlight {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
79
public/styles/jsonTree.css
Normal file
@@ -0,0 +1,79 @@
|
||||
.json-controls {
|
||||
position: sticky;
|
||||
top: -8px;
|
||||
backdrop-filter: blur(5px);
|
||||
padding: 8px;
|
||||
border-bottom: 2px solid var(--theme-window-titlebar-backcolor);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.json-controls button {
|
||||
background: var(--theme-window-backcolor) !important;
|
||||
color: var(--theme-window-color) !important;
|
||||
}
|
||||
|
||||
.json-line {
|
||||
padding: 3px 0;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.json-key {
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
.json-input {
|
||||
padding: 3px 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.json-input.string {
|
||||
color: #032f62;
|
||||
}
|
||||
|
||||
.json-input.number {
|
||||
color: #005cc5;
|
||||
}
|
||||
|
||||
.json-toggle {
|
||||
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||
color: #6f42c1;
|
||||
}
|
||||
|
||||
.json-children {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.json-children.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.json-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.json-add {
|
||||
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||
color: #28a745;
|
||||
opacity: 0.7;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.json-add:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.json-remove {
|
||||
cursor:var(--theme-cursor-pointer) -16 16, pointer;
|
||||
color: #dc3545;
|
||||
opacity: 0.6;
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.json-remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
217
public/styles/os.css
Normal file
@@ -0,0 +1,217 @@
|
||||
/* public/css/desktop.css */
|
||||
body, html { margin:0; padding:0; height:100%; overflow: hidden; font-family: var(--fontFamily); font-size: var(--fontSize); }
|
||||
#desktop { position:relative; height:100vh; overflow:hidden; background-size:var(--theme-desktop-background-size); background-repeat: no-repeat; background-position: center; touch-action: none; }
|
||||
|
||||
#windows { z-index: 1; position:absolute; inset:0; padding:8px; box-sizing:border-box; }
|
||||
.window { position:absolute; width: 800px; height: 600px; border-radius:6px; box-shadow: 0 6px 20px rgba(0,0,0,0.4); overflow:hidden; top:50px; left:50px; display:flex; flex-direction:column; resize:both; }
|
||||
.window-titlebar { padding: 0 0 1px 0; height: 28px; display:flex; justify-content:space-between; align-items:center; }
|
||||
.window-icon { height: 20px; background-size:contain; transform: translate(1px, -1px); background-repeat: no-repeat; background-position: left; background-color: rgb(144, 179, 144); padding:4px;border-radius: 8px;}
|
||||
.window-content { display: flex; flex-direction: column; flex:1; padding:8px; overflow: auto; }
|
||||
.window .controls button { transition: background-color .3s, color .3s; padding: 6px 10px; border:none; }
|
||||
.window[class="max"] .window-resize-handle { display: none; }
|
||||
.window-resize-handle { position:absolute; right:0; bottom:0; width:12px; height:12px; cursor:se-resize; z-index: 10; }
|
||||
.window-resize-n { top: -4px; left: 0; right: 0; height: 8px; cursor: n-resize; }
|
||||
.window-resize-s { bottom: -4px; left: 0; right: 0; height: 8px; cursor: s-resize; }
|
||||
.window-resize-e { right: -4px; top: 0; bottom: 0; width: 8px; cursor: e-resize; }
|
||||
.window-resize-w { left: -4px; top: 0; bottom: 0; width: 8px; cursor: w-resize; }
|
||||
.window-resize-ne { top: -4px; right: -4px; width: 12px; height: 12px; cursor: ne-resize; }
|
||||
.window-resize-nw { top: -4px; left: -4px; width: 12px; height: 12px; cursor: nw-resize; }
|
||||
.window-resize-se { bottom: -4px; right: -4px; width: 12px; height: 12px; cursor: se-resize; }
|
||||
.window-resize-sw { bottom: -4px; left: -4px; width: 12px; height: 12px; cursor: sw-resize; }
|
||||
|
||||
|
||||
#taskbar { z-index: 2; position: absolute; width:100%; bottom:0; left:0; height:42px; overflow:visible; display:flex; flex: 0 0 auto; min-width:0; align-items:center; padding:0 8px; box-sizing:border-box; }
|
||||
#start-btn { transition: background-color 0.3s ease; padding: 8px 15px; border-radius: 5px; border: none; margin-right:8px; }
|
||||
#taskbar-windows { display:flex; gap:6px; align-items:center; flex:1; overflow-y:hidden;overflow-x: auto; min-width: 0; }
|
||||
.taskbar-item { display: flex; position: relative; padding:7px 10px; border-radius:4px; }
|
||||
.taskbar-item::before { content: ''; position: absolute; bottom:1px; left:50%; width:40%; height: 4px; border-radius:4px; transform:translateX(-50%) scaleX(0); transform-origin:center; transition:transform 0.3s ease; }
|
||||
.taskbar-item.focus::before { transform: translateX(-50%) scaleX(1); }
|
||||
|
||||
.notify-button { margin-left:auto; flex: 0 0 auto; display: flex; align-items: center; justify-content: center; }
|
||||
.notify-button.resume, .notify-button.pulse { animation: pulse 1.5s infinite; animation-play-state: running; }
|
||||
.notify-button.pause, .notify-button.pause *, .notify-button.active, .notify-button.active * { animation-play-state: paused; }
|
||||
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.hidden { opacity: 0; pointer-events: none; }
|
||||
|
||||
/* Open submenu vertical */
|
||||
#start-menu { z-index: 2; transition: opacity 0.3s; display:flex; flex-direction:column; position: absolute; bottom: 50px; left: 8px; width: auto; min-width: 300px; border-radius: 8px; overflow: hidden; }
|
||||
.start-header { display: flex; align-items: center; padding: 8px; font-weight: 600; }
|
||||
.start-header > img { height: 24px; width: 24px; margin-right: 5px; }
|
||||
.start-list { list-style: none; margin: 0; padding: 8px 0; height: 60vh; overflow-y: auto; }
|
||||
.start-item { padding: 8px 12px 16px 20px; display: flex; gap: 8px; transition: background 0.2s; border-radius: 4px; }
|
||||
.start-item:not(:has(.submenu)) { padding: 12px;align-items:center; }
|
||||
.start-icon, #start-menu-icon { height: 20px; width: 20px; border-radius: 8px; padding:4px; }
|
||||
.menu-label { display: block; margin-top: 4px; padding-bottom: 0; margin-left: 26px }
|
||||
.start-item.has-submenu { position: relative; display: flex; flex-direction: column; padding-bottom: 8px; }
|
||||
.start-item.has-submenu > .submenu { width: 100%; list-style: none; padding-left: 2px; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; margin: 2px 0 0 8px; }
|
||||
|
||||
.start-item-sys-container { position: relative; left:0; bottom: -8px; padding: 5px 0px; width:100%; display:flex; height:30px; flex-direction:row; justify-content:flex-end; }
|
||||
.start-sys-item { margin: 0 10px 0 8px !important; }
|
||||
|
||||
.start-submenu-head { position: relative; margin-left: 16px; height:32px; display:flex; flex-direction: row; align-items: center; gap:8px; }
|
||||
.start-item.has-submenu > .menu-label::after { content: ""; position: absolute; right: 14px; transform: translateY(50%) rotate(0deg); border: 5px solid transparent; border-top-color: #aaa; transition: transform 0.3s; }
|
||||
.start-item.has-submenu.open > .menu-label::after { content: ""; position: absolute; right: 14px; transform: translateY(50%) rotate(180deg); border: 5px solid transparent; border-top-color: #aaa; transition: transform 0.3s; }
|
||||
.start-item.has-submenu.open > .submenu { max-height: 500px; }
|
||||
.start-item.has-submenu > .submenu li { opacity: 0; transform: translateY(-5px); transition: opacity 0.2s, transform 0.2s; }
|
||||
.start-item.has-submenu.open > .submenu li { opacity: 1; transform: translateY(0); }
|
||||
|
||||
|
||||
img.icon { width: auto; height:20px; object-fit: contain; filter: var(--theme-notifybubble-filter); transform: translate(2px, 2px) }
|
||||
|
||||
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
body, html {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#desktop {
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
/* Fenster = Fullscreen */
|
||||
.window {
|
||||
width: 100% !important;
|
||||
height: calc(100% - 54px) !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
border-radius: 0;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.window-resize-handle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.window-titlebar {
|
||||
height: 29px;
|
||||
padding: 0 0px;
|
||||
}
|
||||
|
||||
.window-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Taskbar größer für Touch */
|
||||
#taskbar {
|
||||
height: 50px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
#start-btn {
|
||||
padding: 10px 14px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.taskbar-item {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.start-list { list-style: none; margin: 0; padding: 8px 0; height: 100%; overflow-y: auto; }
|
||||
|
||||
#start-menu {
|
||||
bottom: 50px !important;
|
||||
height: calc(100dvh - 65px) !important;
|
||||
width: calc(100dvw - 5px);
|
||||
left: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
} */
|
||||
|
||||
.start-item {
|
||||
padding: 14px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.start-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.start-item.has-submenu > .menu-label { margin: 8px 0 0 40px; }
|
||||
|
||||
.start-item-sys-container { margin:auto; bottom: -10px; }
|
||||
|
||||
|
||||
|
||||
/* Buttons besser klickbar */
|
||||
.window .controls button {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
/* Notify Button */
|
||||
.notify-button {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) AND (orientation: landscape) {
|
||||
|
||||
body, html {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#desktop {
|
||||
height: 100dvh;
|
||||
}
|
||||
/* Fenster IMMER fullscreen */
|
||||
.window {
|
||||
width: 100% !important;
|
||||
height: calc(100dvh - 46px) !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Resize komplett aus */
|
||||
.window-resize-handle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.window-titlebar {
|
||||
height: 30px;
|
||||
padding: 0 0px;
|
||||
}
|
||||
|
||||
|
||||
/* Startmenü bis ganz oben */
|
||||
#start-menu {
|
||||
bottom: 44px !important;
|
||||
height: calc(100dvh - 60px) !important;
|
||||
width: 300px;
|
||||
max-width: 35dvw;
|
||||
left: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
/* Taskbar bleibt sichtbar, aber oben drüber sauber */
|
||||
#taskbar {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Startliste nutzt volle Höhe */
|
||||
.start-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.start-item-sys-container { bottom: -10px;}
|
||||
|
||||
|
||||
.start-item.has-submenu > .menu-label { margin: 8px 0 0 28px; }
|
||||
}
|
||||
86
public/styles/table.css
Normal file
@@ -0,0 +1,86 @@
|
||||
/* .table-wrapper { display:grid; grid-column:auto; overflow:auto; min-height:0; min-width:0;} */
|
||||
.table-wrapper { overflow:auto; min-width:0; }
|
||||
|
||||
.table-wrapper.fit-table { grid-template-columns:calc(1fr - 150px); }
|
||||
/* .table-wrapper { flex:1 1 auto; overflow-y:auto; } */
|
||||
/* .table-wrapper.fit-table { width:100%; max-width:100%; } */
|
||||
/* .table-wrapper.fit-table th.word-wrap, .table-wrapper.fit-table td.word-wrap { white-space:normal; word-wrap:break-word; word-break:break-word; overflow-wrap:anywhere; } */
|
||||
|
||||
|
||||
/* #region Dont's */
|
||||
th.no-wrap, td.no-wrap { white-space:nowrap !important; }
|
||||
th.wrap, td.wrap { white-space:wrap; word-wrap: break-word; word-break: keep-all; }
|
||||
table.no-background tr, table.no-background td { background:none !important; }
|
||||
|
||||
table.border * { border:1px solid white; }
|
||||
/* #endregion */
|
||||
|
||||
|
||||
/* #region performance optimizing */
|
||||
table thead { position:sticky; top:0; z-index:20; will-change:transform; transform:translateZ(0);}
|
||||
|
||||
|
||||
/* #endregion */
|
||||
/* echte Tabelle */
|
||||
table { width:calc(100%); border-spacing:0 5px; }
|
||||
table th, table td { min-width:100px; max-width:250px; overflow:hidden; white-space:nowrap; }
|
||||
|
||||
table tr.grouprow:hover { background: rgba(0,0,0,0.05);}
|
||||
|
||||
/* Header sticky — aber innerhalb echter Tabelle */
|
||||
thead th { position:sticky; top:var(--filter-height); }
|
||||
|
||||
/* KEIN display:block mehr! */
|
||||
thead, tbody { display:table-row-group; }
|
||||
|
||||
|
||||
table thead th { padding:5px; }
|
||||
/* table tbody td { padding:5px 0px 5px 20px; } */
|
||||
table tbody td:not(:first-child):not(:last-child), table thead th:not(:first-child):not(:last-child) { border-width:0; border-style:solid; }
|
||||
table tbody tr.grouprow { font-weight:700; }
|
||||
|
||||
table .text-align\:center { text-align:center; }
|
||||
table .text-align\:right { text-align:right; }
|
||||
table .text-align\:left { text-align:left; }
|
||||
|
||||
td { overflow:hidden; text-overflow:ellipsis; /* verhindert, dass Inhalt die Zelle sprengt */ }
|
||||
|
||||
.table-filter-container {
|
||||
border-bottom-width:8px;
|
||||
border-bottom-style:solid;
|
||||
display:flex;
|
||||
justify-content:flex-start;
|
||||
flex-direction:column;
|
||||
flex-wrap:wrap;
|
||||
gap:10px;
|
||||
|
||||
position:sticky;
|
||||
left:0px;
|
||||
top:0px;
|
||||
/* z-index:20; */
|
||||
padding:5px 10px;
|
||||
border-radius:var(--border-raduis) var(--border-raduis) 0 0;
|
||||
}
|
||||
.table-filter-container .live-counter { position:absolute; right:18px; margin-left:auto; font-weight:bold; }
|
||||
.table-filter-container input, .table-filter-container select { padding:5px !important; }
|
||||
|
||||
|
||||
th.sort-asc::after {
|
||||
content:" ▲";
|
||||
}
|
||||
|
||||
th.sort-desc::after {
|
||||
content:" ▼";
|
||||
}
|
||||
|
||||
|
||||
td.vote { width:28px; height:28px; position:relative; padding:0; }
|
||||
|
||||
td.vote:hover { background:rgba(0,0,0,0.05); /* cursor:pointer;*/ }
|
||||
td.vote::before,
|
||||
td.vote::after { position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); }
|
||||
td.yes::before { content:""; width:8px; height:16px; border:solid #2ecc71; border-width:0 3px 3px 0; transform:translate(-50%, -60%) rotate(45deg); }
|
||||
td.no::before, td.no::after { content:""; width:3px; height:18px; background:#e74c3c; }
|
||||
td.no::before { transform:translate(-50%, -50%) rotate(45deg); }
|
||||
td.no::after { transform:translate(-50%, -50%) rotate(-45deg); }
|
||||
td.maybe::before { content:""; width:0px; height:0px; border-radius:50%; border:10px solid #f1c40f; transform:translate(-50%, -50%); }
|
||||
67
public/styles/userNotification.css
Normal file
@@ -0,0 +1,67 @@
|
||||
/* Bubble-Container */
|
||||
#notify-bubble {
|
||||
position: absolute;
|
||||
bottom: 125%;
|
||||
right: 3px;
|
||||
min-width: 200px;
|
||||
max-height: 50vh;
|
||||
width: max-content; /* 🔑 wächst mit Inhalt */
|
||||
max-width: 600px; /* aber capped */
|
||||
background: var(--theme-taskbar-tray-backcolor);
|
||||
color: var(--theme-taskbar-tray-color);
|
||||
border-color: var(--theme-taskbar-tray-border-color);
|
||||
border-radius: 10px;
|
||||
overflow-y: auto; /* vertikal scrollen */
|
||||
overflow-x: hidden; /* horizontal verhindern */
|
||||
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
|
||||
padding: 10px 10px 6px 10px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(10px);
|
||||
transition: all 0.25s ease;
|
||||
z-index: 99999999;
|
||||
text-align: end;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* Bubble-Items als Grid */
|
||||
.bubble-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, auto) 250px;
|
||||
gap: 10px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
/* cursor: pointer; */
|
||||
align-items: center;
|
||||
transition: transform 0.25s;
|
||||
|
||||
}
|
||||
|
||||
/* linke Spalte */
|
||||
.bubble-item > :nth-child(1) {
|
||||
min-width: 0; /* wichtig für Grid Shrink */
|
||||
white-space: normal; /* Text darf umbrechen */
|
||||
overflow-wrap: normal; /* Wörter nur bei Bedarf umbrechen */
|
||||
word-break: normal; /* nicht mitten in Wörtern */
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* rechte Spalte fix */
|
||||
.bubble-item > :nth-child(2) {
|
||||
width: 250px;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* hover Effekt */
|
||||
.bubble-item:hover {
|
||||
background: rgba(64,64,64,0.4);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
/* Bubble sichtbar machen */
|
||||
.notify-button.active #notify-bubble {
|
||||
pointer-events: auto;
|
||||
opacity:1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
153
public/views/desktop.hbs
Normal file
@@ -0,0 +1,153 @@
|
||||
<head>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="javascript/main.js"></script>
|
||||
<script src="javascript/customModal.js"></script>
|
||||
<script src="javascript/pluginAPI.js"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="styles/default.css">
|
||||
<link rel="stylesheet" href="styles/contextMenu.css">
|
||||
<link rel="stylesheet" href="styles/userNotification.css">
|
||||
<link rel="stylesheet" href="styles/table.css">
|
||||
<link rel="stylesheet" href="styles/jsonTree.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="styles/colors.css" />
|
||||
<link rel="stylesheet" href="styles/os.css" />
|
||||
|
||||
<div id="message-container"></div>
|
||||
<div id="copy-toast"></div>
|
||||
<title>Radix OS</title>
|
||||
</head>
|
||||
|
||||
<div id="desktop">
|
||||
<div id="windows"></div>
|
||||
|
||||
<!-- Startmenu -->
|
||||
<div id="start-menu" class="hidden">
|
||||
<div class="start-header"><img id="start-menu-icon"><span>Radix OS</span></div>
|
||||
<ul class="start-list">
|
||||
{{#groupBy startMenuItems "section"}}
|
||||
|
||||
<li class="start-submenu-head">
|
||||
<span><i>{{this.key}}</i></span>
|
||||
</li>
|
||||
{{#each items}}
|
||||
{{#ifSingle this.menu.items}}
|
||||
{{#if this.authorized}}
|
||||
<li class="start-item {{#unless ../this.active}}unload{{/unless}}" data-active="{{#equaler ../this.active "&&" this.authorized}}true{{else}}false{{/equaler}}" data-appname="{{../this.name}}" data-appview="{{this.view}}" data-viewlabel="{{this.label}}">
|
||||
{{#if this.icon}}
|
||||
<img src="{{#if ../this.pluginPath}}/{{../this.name}}{{/if}}/images/{{this.icon}}" class="start-icon" />
|
||||
{{else}}
|
||||
{{/if}}
|
||||
<span>{{this.label}}</span>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<li class="start-item has-submenu">
|
||||
<img src="{{#if ../this.pluginPath}}/{{../this.name}}{{/if}}/images/folder.png" class="start-icon" style="position:absolute;left:12px;"/>
|
||||
<span class="menu-label">{{this.menu.label}}</span>
|
||||
{{!-- {{#if this.version}}<small>v{{this.version}}</small>{{/if}} --}}
|
||||
|
||||
<ul class="submenu">
|
||||
{{#each this.menu.items}}
|
||||
{{#equaler this.label "==" "hr"}}
|
||||
<li><hr></li>
|
||||
{{else}}
|
||||
{{#if this.authorized}}
|
||||
<li class="start-item {{#unless ../this.active}}unload{{/unless}}" data-active="{{#equaler ../this.active "&&" this.authorized}}true{{else}}false{{/equaler}}" data-appname="{{../this.name}}" data-appview="{{this.view}}" data-viewlabel="{{this.label}}">
|
||||
{{#if this.icon}}
|
||||
<img src="{{#if ../this.pluginPath}}/{{../this.name}}{{/if}}/images/{{this.icon}}" class="start-icon" />
|
||||
{{else}}
|
||||
{{/if}}
|
||||
<span>{{this.label}}</span>
|
||||
{{#if this.version}}<small>v{{this.version}}</small>{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/equaler}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</li>
|
||||
{{/ifSingle}}
|
||||
{{/each}}
|
||||
{{/groupBy}}
|
||||
{{!-- [Function restart] in javascript/main.js --}}
|
||||
</ul>
|
||||
|
||||
<div class="start-item-sys-container">
|
||||
<button class="monolyth start-sys-item" data-tooltip="Neustart" onclick="restart();" data-tooltip-mode="always">⟳</button>
|
||||
<button class="monolyth start-sys-item" data-tooltip="Abmelden" onclick="logout()" data-tooltip-mode="always">🔓</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Taskbar -->
|
||||
<div id="taskbar">
|
||||
<button id="start-btn">☰</button>
|
||||
<div id="taskbar-windows"></div>
|
||||
<button style="margin-right:0;" class="monolyth notify-button pulse">
|
||||
<img class="icon" src="/images/notifybubble.png">
|
||||
<div id="notify-bubble"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="javascript/tutorial.js"></script>
|
||||
<script src="javascript/notifyBubble.js"></script>
|
||||
<script src="javascript/contextMenu.js"></script>
|
||||
<script src="javascript/tableFilter.js"></script>
|
||||
<script src="javascript/requiredFields.js"></script>
|
||||
<script src="javascript/loadOnce.js"></script>
|
||||
<script src="javascript/JSON.js"></script>
|
||||
<script src="javascript/os.js"></script>
|
||||
<script>
|
||||
addRestartHook(() => localStorage.setItem('openWindows', JSON.stringify({ })));
|
||||
|
||||
fetch('/api/Plugins/loadScripts', { method: 'POST', headers: {'Content-Type': 'application/json'} })
|
||||
.then(res => res.json())
|
||||
.then(scripts => {
|
||||
(scripts).forEach(script => {
|
||||
reloadPluginScript(script);
|
||||
});
|
||||
});
|
||||
|
||||
loadServerStyles(getCookie('theme'));
|
||||
|
||||
const notifyButtons = document.querySelectorAll('.notify-button');
|
||||
const trayNotifyButton = document.querySelector('#taskbar > .notify-button')
|
||||
const notify = new NotifyBubble(trayNotifyButton, "#notify-bubble");
|
||||
|
||||
document.addEventListener("contextmenu", evt => evt.preventDefault());
|
||||
|
||||
document.querySelectorAll('#start-menu .start-item.has-submenu').forEach(item => {
|
||||
item.addEventListener('click', evt => {
|
||||
// Toggle nur dieses Submenu
|
||||
item.classList.toggle('open');
|
||||
});
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
const response = await fetch('/logout', { method: 'POST', headers: {'Content-Type': 'application/json'} });
|
||||
if (response.ok) {
|
||||
window.history.pushState({ }, '', '/login');
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
fetch('/api/NotifyTray/getTrays')
|
||||
.then(res => res.json())
|
||||
.then(trays => {
|
||||
notify.clear();
|
||||
if (trays.length > 0) {
|
||||
trays.forEach(tray => {
|
||||
notify.addItem(tray, () => {
|
||||
if(tray.JSON) {
|
||||
//execution of tray action, e.g. open window
|
||||
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
127
public/views/eventlog.hbs
Normal file
@@ -0,0 +1,127 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Event Log</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card static">
|
||||
<div class="table-wrapper">
|
||||
<table id="eventTable" >
|
||||
<thead class="no-wrap">
|
||||
<tr>
|
||||
<th class="text-align:left" onclick="">ID</th>
|
||||
<th class="text-align:left" onclick="">Datum</th>
|
||||
<th class="text-align:left" onclick="">Level</th>
|
||||
<th class="text-align:left" onclick="">Plugin</th>
|
||||
<th class="text-align:left" onclick="">Message</th>
|
||||
<th class="text-align:left" onclick="">Trace</th>
|
||||
<th class="text-align:left" onclick="">User</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each logs}}
|
||||
<tr>
|
||||
<td>{{this.ID}}</td>
|
||||
<td class="no-wrap">{{dateFormat this.Date "yyyy-mm-dd HH:MM:SS"}}</td>
|
||||
<td class="{{this.Level_ID}}">
|
||||
{{LevelDisplayName}}
|
||||
</td>
|
||||
<td>{{this.PluginName}}</td>
|
||||
<td><span>{{replaceAll this.Message "||" "<br>"}}</span> <span class="copy-icon" onclick="copyToClipboard(`{{replaceAll this.Message "||" "<br>"}}`)">⧉</span></td>
|
||||
<td>{{this.Trace}}</td>
|
||||
<td class="no-wrap">{{this.ClearTextUser}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid" style="pointer-events:auto;position:absolute; bottom:18px;right:18px;grid-template-columns: 1fr;">
|
||||
<button class="redbutton" id="confirmClearLog" data-tooltip="Löscht alle Einträge aus der Datenbank">Log leeren</button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
const eventTable = document.getElementById("eventTable");
|
||||
let mappedLogs = [];
|
||||
|
||||
|
||||
confirmClearLog.onclick = evt => {
|
||||
feedbox(
|
||||
{ title: `Hast Du Sie nicht mehr Alle?`,
|
||||
message: `<p>Damit werden alle Einträge aus dem EventLog entfernt!</p>`,
|
||||
buttons: {
|
||||
yes: {
|
||||
text: 'Ich weiß, <b>Geht auf meinen Nacken!</b>',
|
||||
onClick: async () => await clearLog()
|
||||
},
|
||||
no: {
|
||||
text: 'Dazu habe ich nicht die Eier'
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function clearLog() {
|
||||
const res = await fetch(`/api/eventlog/clearlog`, { method: 'POST' });
|
||||
const data = await res.json();
|
||||
vt.source([]);
|
||||
}
|
||||
|
||||
|
||||
const vt = virtualTable({
|
||||
tableEl: eventTable,
|
||||
rowHeight: 40,
|
||||
buffer: 5,
|
||||
groupKey: null, // optional zum Gruppieren
|
||||
filterConfig:{
|
||||
exceptedColumns: [''],
|
||||
columnModes:{
|
||||
Plugin: 'dropdown',
|
||||
Level: 'dropdown',
|
||||
User: 'dropdown'
|
||||
},
|
||||
checkboxFilter:{
|
||||
column:'Level_ID',
|
||||
rules:[
|
||||
{ label:'Erfolgreich', test:v => parseInt(v) === 0 },
|
||||
{ label:'Information', test:v => parseInt(v) === 1 },
|
||||
{ label:'Warnung', test:v => parseInt(v) === 2 },
|
||||
{ label:'Fehler', test:v => parseInt(v) === 4 },
|
||||
{ label:'Absturz', test:v => parseInt(v) === 8 },
|
||||
]
|
||||
}
|
||||
},
|
||||
customRender: (row, tr) => {
|
||||
tr.style.height = '40px';
|
||||
createTd(tr, row['ID'], { classes: [ 'text-align:left'] });
|
||||
createTd(tr, row['Datum']);
|
||||
createTd(tr, row['Level']);
|
||||
createTd(tr, row['Plugin']);
|
||||
createTd(tr, row['Message'], { classes: [ 'no-wrap' ], attributes: { 'data-tooltip': formatHtml(row['Message']) } });
|
||||
createTd(tr, row['Trace'], { attributes: { 'data-tooltip': formatHtml(row['Trace']) } });
|
||||
createTd(tr, row['User']);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
fetch('/api/eventlog/getLogs', { method: 'POST' })
|
||||
.then(logs => logs.json())
|
||||
.then(logs => {
|
||||
mappedLogs = logs.map(row => ({ ...row, Plugin: row['PluginName'], Level: row['LevelDisplayName'], Datum: dateFormat(row['Date'], 'yyyy-mm-dd HH:MM:SS'), User: row['ClearTextUser'] }))
|
||||
vt.addData(mappedLogs.length != 0 ? mappedLogs : {});
|
||||
});
|
||||
|
||||
|
||||
adminSocket.on('eventlog_table', (data) => {
|
||||
data = {...data, Plugin: data['PluginName'], Level: data['LevelDisplayName'], Datum: dateFormat(data['Date'], 'yyyy-mm-dd HH:MM:SS'), User: data['ClearTextUser'] };
|
||||
mappedLogs.unshift(data)
|
||||
vt.source(mappedLogs);
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
36
public/views/help/Hilfe.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
Bilder, Symbole und Zeichnungen wurden durch KI generiert und teilweise aus bereits existierenden, lizenzierten Quellen im Auftrag der Stadt Frankfurt am Main verwendet.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Der Programmcode wurde ebenfalls durch KI unterstützend erstellt. Dies betrifft ausschließlich die Darstellungs- und Designlogik. Alle Verarbeitungen personenbezogener Daten liegen in der Obhut des nutzenden Betriebs.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Alle Inhalte der in RadixOS enthaltenen Plugins dienen ausschließlich dem dienstlichen Betrieb sowie der Weiterentwicklung interner Machine-Learning-Prozesse
|
||||
<i>(automatisiertes Lernen computergestützter Verfahren)</i>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Datenbanken, Inhalte sowie personenbezogene Daten werden nicht an Dritte weitergegeben oder öffentlich zugänglich gemacht. RadixOS ist als geschlossenes System konzipiert.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b style="color:var(--theme-accent-default-backcolor)">
|
||||
Das Urheberrecht an – RADIX OS – liegt beim Entwickler
|
||||
</b>
|
||||
|
||||
</p>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
</html>
|
||||
11
public/views/integrated/development.hbs
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
test
|
||||
</body>
|
||||
</html>
|
||||
106
public/views/integrated/help.hbs
Normal file
@@ -0,0 +1,106 @@
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hilfe</title>
|
||||
|
||||
<style>
|
||||
|
||||
ul#help li a,
|
||||
ul#help li a:visited {
|
||||
text-decoration: none;
|
||||
color: var(--theme-container-card-color);
|
||||
}
|
||||
|
||||
ul#help {
|
||||
display:flex;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
flex-wrap: nowrap;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
ul#help li {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container static" style="height: 100vh;">
|
||||
|
||||
<div id="helpTabs" class="tabs" style="overflow-y: auto;scrollbar-width: thin; padding:0;flex: 0 0 auto;"></div>
|
||||
|
||||
|
||||
<div class="card static" style="overflow-y:auto;flex: 1 1 auto;" >
|
||||
<div id="tabWrapper" class="tab-contents" ></div>
|
||||
</div>
|
||||
<div id="helpFooter" class="card static" style="height:auto;bottom:0;text-align:left;flex:0 0 auto;flex-direction:row;justify-content:space-between;gap:4px">
|
||||
<div class="selectable">
|
||||
<div style="color:var(--theme-accent-default-backcolor);font-weight:bold;">
|
||||
<span>© Radix OS</span> <span id="year"></span>
|
||||
<span>Manuel Sowada</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="plugin"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
|
||||
function showTabs() {
|
||||
fetch(`/api/help/getTabs`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then(async data => {
|
||||
data.forEach(async t => {
|
||||
const tabElement = document.createElement('div');
|
||||
tabElement.className = 'tab';
|
||||
tabElement.dataset.tab = t.name;
|
||||
tabElement.textContent = t.name;
|
||||
tabElement.addEventListener('click', async () => {
|
||||
Array.from(document.querySelectorAll('.tab')).forEach(t => t.classList.remove('active'));
|
||||
tabElement.classList.add('active');
|
||||
tabWrapper.innerHTML = '';
|
||||
|
||||
const response = await fetch('/api/help/getHelp', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ name: t.name })
|
||||
});
|
||||
const item = await response.json();
|
||||
|
||||
const html = `
|
||||
<div>
|
||||
<p>${response.status === 500 ? 'Hilfe nicht verfügbar' : item.html}</p>
|
||||
${item.description ? `<hr /><p>${item.description}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const container = document.querySelector('#tabWrapper');
|
||||
container.innerHTML = html;
|
||||
});
|
||||
document.getElementById('helpTabs').appendChild(tabElement);
|
||||
})
|
||||
})
|
||||
}
|
||||
showTabs();
|
||||
|
||||
year.textContent = new Date().getFullYear();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
35
public/views/integrated/serverconfig.hbs
Normal file
@@ -0,0 +1,35 @@
|
||||
<header>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</header>
|
||||
|
||||
<body>
|
||||
<div class="container static">
|
||||
<div class="card" style="overflow: auto;height:100vh">
|
||||
<div id="jsonConfigTree" class="json-tree"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
<script>
|
||||
fetch('/api/getConfig', { method: 'POST' })
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
const tree = createJsonTree({
|
||||
container: document.getElementById("jsonConfigTree"),
|
||||
data: json,
|
||||
expandInitially: true,
|
||||
onSave: json => {
|
||||
fetch('/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(json, null, 2)
|
||||
}).then(() => writeEventLog(0, 'Serverconfig', tree.getChanges()) );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
101
public/views/integrated/serverinfo.hbs
Normal file
@@ -0,0 +1,101 @@
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<span style="font-weight: bold;">Dienst</span>
|
||||
<button class="redbutton" id="shutdownButton">Abschalten</button>
|
||||
<button class="yellowbutton" id="restartButton">Neustart</button>
|
||||
<div>
|
||||
<span>PID:</span> <span class="selectable" id="pid"></span>
|
||||
<span class="copy-icon" onclick="copyToClipboard(`${document.getElementById('pid').textContent}`)">⧉</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="table-wrapper" style="max-height: 350px;">
|
||||
<table id="releaseNotes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-align:left">Erledigt</th>
|
||||
<th class="text-align:left">Timestamp</th>
|
||||
<th class="text-align:left">User</th>
|
||||
<th class="text-align:left">Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div id="package" style="overflow:auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
shutdownButton.onclick = evt => {
|
||||
fetch('/api/shutdown', { method: 'POST' })
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
console.log(json);
|
||||
});
|
||||
};
|
||||
|
||||
restartButton.onclick = evt => {
|
||||
fetch('/api/restart', { method: 'POST' })
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
console.log(json);
|
||||
});
|
||||
};
|
||||
|
||||
fetch('/api/getServerInfo', { method: 'POST' })
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
|
||||
document.querySelector('#pid').innerHTML = json.pid;
|
||||
|
||||
|
||||
const vt = virtualTable({
|
||||
tableEl: document.getElementById('releaseNotes'),
|
||||
data: [],
|
||||
buffer: 5,
|
||||
rowHeight: 30,
|
||||
filterConfig:{
|
||||
exceptedColumns: [ 'Erledigt' ],
|
||||
columnModes: {
|
||||
'datetime': 'text',
|
||||
'sAMAccountName': 'text',
|
||||
'Note': 'text'
|
||||
},
|
||||
checkboxFilter: {
|
||||
column: 'finish',
|
||||
rules: [
|
||||
{ label: 'Nur erledigte', test: v => v === true },
|
||||
{ label: 'Nur unerledigte', test: v => v === false },
|
||||
]
|
||||
}
|
||||
},
|
||||
customRender: (row, tr) => {
|
||||
createTd(tr,
|
||||
`
|
||||
<label class="cb cb-modern">
|
||||
<input id="id1" data-status="{{name}}" type="checkbox" ${row.finish ? 'checked' : ''} >
|
||||
<span class="cb-box" aria-hidden="true"></span>
|
||||
</label>
|
||||
`, { });
|
||||
createTd(tr, row.datetime, { });
|
||||
createTd(tr, row.sAMAccountName, { });
|
||||
createTd(tr, row.value, { attributes: { "data-tooltip": row.value } });
|
||||
}
|
||||
});
|
||||
|
||||
vt.addData(json.releaseNotes)
|
||||
document.querySelector('#package').innerHTML = JSON.stringify(json.package).split(',').join('<br>');
|
||||
/*
|
||||
createJsonTree({
|
||||
container: document.getElementById("package-json"),
|
||||
data: json,
|
||||
expandInitially: true,
|
||||
onSave: () => { }
|
||||
});
|
||||
*/
|
||||
});
|
||||
</script>
|
||||
30
public/views/integrated/styleconfig.hbs
Normal file
@@ -0,0 +1,30 @@
|
||||
<header>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</header>
|
||||
|
||||
|
||||
<div style="width:100%;height:calc(100% - 64px);">
|
||||
<div id="jsonStyleTree" class="json-tree"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
fetch('/api/getStyles', { method: 'POST' })
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
const tree = createJsonTree({
|
||||
container: document.getElementById("jsonStyleTree"),
|
||||
data: json,
|
||||
expandInitially: true,
|
||||
onSave: json => {
|
||||
fetch('/style', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(json, null, 2)
|
||||
}).then(() => writeEventLog(0, 'Stylesheet', JSON.stringify(tree.getChanges())) );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
113
public/views/integrated/usersettings.hbs
Normal file
@@ -0,0 +1,113 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<label><b>Farbgebung</b></label>
|
||||
<select id="themeSwitch" style="width: 100%;">
|
||||
<option value="dark">Dunkel</option>
|
||||
<option value="light">Hell</option>
|
||||
<option value="modern">Modern</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<label><b>Schrift</b></label>
|
||||
<select id="fontSelector" style="width: 100%;">
|
||||
<optgroup label="Sans-Serif (Windows Standard)">
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Calibri">Calibri</option>
|
||||
<option value="Candara">Candara</option>
|
||||
<option value="Segoe UI">Segoe UI</option>
|
||||
<option value="Tahoma">Tahoma</option>
|
||||
<option value="Trebuchet MS">Trebuchet MS</option>
|
||||
<option value="Verdana">Verdana</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup label="Serif (Windows Standard)">
|
||||
<option value="Cambria">Cambria</option>
|
||||
<option value="Constantia">Constantia</option>
|
||||
<option value="Georgia">Georgia</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Palatino Linotype">Palatino Linotype</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup label="Monospace / Fixed-Width">
|
||||
<option value="Consolas">Consolas</option>
|
||||
<option value="Courier New">Courier New</option>
|
||||
<option value="Lucida Console">Lucida Console</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup label="Fun / Decorative (Windows enthält einige)">
|
||||
<option value="Comic Sans MS">Comic Sans MS</option>
|
||||
<option value="Impact">Impact</option>
|
||||
<option value="Segoe Script">Segoe Script</option>
|
||||
<option value="Segoe Print">Segoe Print</option>
|
||||
</optgroup>
|
||||
|
||||
</select>
|
||||
<div style="display:flex;flex-direction:row;align-items:center;">
|
||||
<input type="range" id="sizeSlider" min="10" max="32" value="18" style="width:100%;">
|
||||
<span id="sizeValue">18px</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="display:flex;flex:1;flex-direction:column;">
|
||||
<label><b>Vollbildmodus</b></label>
|
||||
<button class="bluebutton" id="fullscreenBtn">Einschalten</button>
|
||||
<label>
|
||||
Der Vollbildmodus hat den Vorteil, dass der komplette Bildschirm mit dieser Webseite ausgefüllt wird.<br><i style="color:var(--theme-accent-default-backcolor)">Sehr nützlich für mehr als einen Bildschirme</i>
|
||||
</label>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
const themeSwitch = document.getElementById("themeSwitch");
|
||||
const selector = document.getElementById("fontSelector");
|
||||
const slider = document.getElementById("sizeSlider");
|
||||
const label = document.getElementById("sizeValue");
|
||||
const fullScreenBtn = document.getElementById("fullscreenBtn");
|
||||
|
||||
fullScreenBtn.addEventListener("click", goFullscreen);
|
||||
function goFullscreen() {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
document.documentElement.requestFullscreen();
|
||||
}
|
||||
fullScreenBtn.textContent = !document.fullscreenElement ? "Ausschalten" : "Einschalten";
|
||||
}
|
||||
|
||||
if(savedTheme) { themeSwitch.value = savedTheme; }
|
||||
if(savedFontFamily) { selector.value = savedFontFamily; }
|
||||
if(savedFontSize) { slider.value = savedFontSize; label.textContent = savedFontSize + 'px'; }
|
||||
|
||||
|
||||
themeSwitch.addEventListener('change', (evt) => {
|
||||
const theme = evt.target.value;
|
||||
switchTheme(theme);
|
||||
})
|
||||
|
||||
|
||||
function applySettings() {
|
||||
switchTheme(themeSwitch.value);
|
||||
setFontFamily(selector.value);
|
||||
setFontSize(slider.value)
|
||||
}
|
||||
applySettings();
|
||||
|
||||
selector.addEventListener("change", () => {
|
||||
setFontFamily(selector.value);
|
||||
});
|
||||
|
||||
|
||||
slider.addEventListener("input", () => {
|
||||
setFontSize(parseInt(slider.value))
|
||||
label.textContent = slider.value + 'px';
|
||||
});
|
||||
|
||||
|
||||
|
||||
// jede option bekommt ihre eigene Schrift
|
||||
[...selector.options].forEach(option => {
|
||||
option.style.fontFamily = option.value;
|
||||
});
|
||||
</script>
|
||||
9
public/views/layouts/default.hbs
Normal file
@@ -0,0 +1,9 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
{{{body}}}
|
||||
</body>
|
||||
</html>
|
||||
187
public/views/login.hbs
Normal file
@@ -0,0 +1,187 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script src="javascript/main.js"></script>
|
||||
<script src="javascript/customModal.js"></script>
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="styles/default.css">
|
||||
<link rel="stylesheet" href="styles/contextMenu.css">
|
||||
<link rel="stylesheet" href="styles/userNotification.css">
|
||||
<link rel="stylesheet" href="styles/table.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="styles/colors.css" />
|
||||
<link rel="stylesheet" href="styles/os.css" />
|
||||
|
||||
<div id="message-container"></div>
|
||||
<div id="copy-toast"></div>
|
||||
<title>Radix OS</title>
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body id="desktop">
|
||||
{{!-- <form id="login-form" action="/login" method="POST"> --}}
|
||||
<form id="login-form" class="container" style="width:50vw;height:100vh;align-content:center;">
|
||||
<div class="static card" style="flex-direction:column">
|
||||
<label for="sAMAccountName">Benutzername:</label>
|
||||
<input type="text" {{!-- autocomplete="username" --}} id="sAMAccountName" name="sAMAccountName" placeholder="< Vorname.Nachname >" data-tooltip="Melde dich mit deinem Windows-Benutzernamen an.<br>Ist dies deine erste Anmeldung, dann vergib ein neues Kennwort!<br><i style='color:green;'>Hat keine Auswirkungen auf deine Windows-Anmeldung</i>" required>
|
||||
|
||||
<label for="password">Passwort:</label>
|
||||
<input type="password" autocomplete="current-password" id="password" name="password" required>
|
||||
<br>
|
||||
<button class="bluebutton" type="submit">Einloggen</button>
|
||||
</div>
|
||||
</form>
|
||||
<button class="yellowbutton" id="openSettings" type="button" style="position:absolute;top:10px;right:10px;">Einstellungen</button>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
|
||||
<script src="javascript/notifyBubble.js"></script>
|
||||
<script src="javascript/tableFilter.js"></script>
|
||||
<script src="javascript/requiredFields.js"></script>
|
||||
<script src="javascript/loadOnce.js"></script>
|
||||
<script src="javascript/JSON.js"></script>
|
||||
<script type="text/javascript">
|
||||
const sAMAccountName = document.querySelector('#sAMAccountName');
|
||||
const form = document.getElementById('login-form');
|
||||
|
||||
|
||||
loadServerStyles(getCookie('theme'));
|
||||
setCSSVariable('fontFamily', getCookie('fontFamily') ?? 'Verdana');
|
||||
setCSSVariable('fontSize', getCookie('fontSize') ?? '14');
|
||||
setCSSVariable('theme', getCookie('theme') ?? 'light');
|
||||
|
||||
|
||||
form.addEventListener('submit', async e => {
|
||||
try {
|
||||
e.preventDefault(); // verhindert Reload
|
||||
let error = null;
|
||||
const data = new FormData(form);
|
||||
const obj = Object.fromEntries(data.entries());
|
||||
|
||||
// Sende Daten via Fetch an Backend
|
||||
const resSendLoginData = await fetch('/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(obj)
|
||||
});
|
||||
|
||||
if(resSendLoginData.ok) {
|
||||
const loginData = await resSendLoginData.json();
|
||||
|
||||
if(resSendLoginData.status === 200) {
|
||||
setTimeout(() => {
|
||||
window.history.pushState({ }, '', '/');
|
||||
location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
if(resSendLoginData.status === 401) {
|
||||
|
||||
}
|
||||
showMessage('Login', `${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}<br><br>${loginData.message}`, data.levelId, () => { window.history.pushState({ }, '', '/'); location.reload(); } , 5000);
|
||||
} else {
|
||||
const errorData = await resSendLoginData.json();
|
||||
showMessage('Login', `${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}<br><br>${errorData.message}`, errorData.levelId, () => { window.history.pushState({ }, '', '/'); location.reload(); } , 5000);
|
||||
}
|
||||
} catch(err) {
|
||||
alert(err)
|
||||
}
|
||||
});
|
||||
|
||||
sAMAccountName.addEventListener('blur', async () => {
|
||||
if(sAMAccountName.value.trim() === '') {
|
||||
sAMAccountName.classList.remove('valid', 'invalid');
|
||||
sAMAccountName.removeAttribute('data-tooltip');
|
||||
sAMAccountName.setAttribute('data-tooltip', "Melde dich mit deinem Windows-Benutzernamen an.<br>Ist dies deine erste Anmeldung, dann vergib ein neues Kennwort!<br><i style='color:green;'>Hat keine Auswirkungen auf deine Windows-Anmeldung</i>")
|
||||
return;
|
||||
}
|
||||
const responseVerifyAccount = await fetch('/checkLoginName', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sAMAccountName: sAMAccountName.value })
|
||||
});
|
||||
if(responseVerifyAccount.ok || responseVerifyAccount.status === 200) {
|
||||
sAMAccountName.classList.add('valid');
|
||||
sAMAccountName.setAttribute('data-tooltip', `Du kannst dich mit dem Benutzername <b style="color:green;">${sAMAccountName.value}</b> anmelden`)
|
||||
} else {
|
||||
sAMAccountName.classList.add('invalid');
|
||||
sAMAccountName.setAttribute('data-tooltip', `Der Benutzername <b style="color:red;">${sAMAccountName.value}</b> wurde nicht gefunden`)
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('#openSettings').onclick = async evt => {
|
||||
feedbox(
|
||||
{ title: `Einstellungen`,
|
||||
message: /*html*/`
|
||||
<div class="container">
|
||||
<div class="card static">
|
||||
<label>Farbgebung</label>
|
||||
<select id="themeSwitch" onchange="switchTheme(this.value)">
|
||||
<option value="dark" ${getCookie('theme') == 'dark' ? 'selected' : ''}>Dunkel</option>
|
||||
<option value="light" ${getCookie('theme') == 'light' ? 'selected' : ''}>Hell</option>
|
||||
<option value="modern" ${getCookie('theme') == 'modern' ? 'selected' : ''}>Modern</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card static">
|
||||
<label>Schrift</label>
|
||||
<select id="fontSelector" size="8" onchange="setFontFamily(this.value)">
|
||||
<optgroup label="Sans-Serif (Windows Standard)">
|
||||
<option style="font-family:Arial" value="Arial">Arial</option>
|
||||
<option style="font-family:Calibri" value="Calibri">Calibri</option>
|
||||
<option style="font-family:Candara" value="Candara">Candara</option>
|
||||
<option style="font-family:Segoe UI" value="Segoe UI">Segoe UI</option>
|
||||
<option style="font-family:Tahoma" value="Tahoma">Tahoma</option>
|
||||
<option style="font-family:Trebuchet MS" value="Trebuchet MS">Trebuchet MS</option>
|
||||
<option style="font-family:Verdana" value="Verdana">Verdana</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup label="Serif (Windows Standard)">
|
||||
<option style="font-family:Cambria" value="Cambria">Cambria</option>
|
||||
<option style="font-family:Constantia" value="Constantia">Constantia</option>
|
||||
<option style="font-family:Georgia" value="Georgia">Georgia</option>
|
||||
<option style="font-family:Times New Roman" value="Times New Roman">Times New Roman</option>
|
||||
<option style="font-family:Palatino Linotype" value="Palatino Linotype">Palatino Linotype</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup label="Monospace / Fixed-Width">
|
||||
<option style="font-family:Consolas" value="Consolas">Consolas</option>
|
||||
<option style="font-family:Courier New" value="Courier New">Courier New</option>
|
||||
<option style="font-family:Lucida Console" value="Lucida Console">Lucida Console</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup label="Fun / Decorative (Windows enthält einige)">
|
||||
<option style="font-family:Comic Sans MS" value="Comic Sans MS">Comic Sans MS</option>
|
||||
<option style="font-family:Impact" value="Impact">Impact</option>
|
||||
<option style="font-family:Segoe Script" value="Segoe Script">Segoe Script</option>
|
||||
<option style="font-family:Segoe Print" value="Segoe Print">Segoe Print</option>
|
||||
</optgroup>
|
||||
|
||||
</select>
|
||||
<div style="display:flex;flex-direction:row;align-items:center">
|
||||
<input type="range" id="sizeSlider" min="10" max="32" value="${savedFontSize ?? '14'}" onchange="setFontSize(parseInt(this.value)); sizeValue.textContent = this.value + 'px';">
|
||||
<span id="sizeValue">${savedFontSize ?? '14'}px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
buttons: {
|
||||
yes: {
|
||||
text: '<b>Fertig</b>',
|
||||
onClick: async () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
||||
|
||||
16
public/views/partials/child.hbs
Normal file
@@ -0,0 +1,16 @@
|
||||
<div class="window" data-winid="{{name}}-{{@index}}" data-plugin="{{name}}">
|
||||
<div class="window-titlebar">
|
||||
<div class="title">{{{viewLabel}}}</div>
|
||||
<div class="controls">
|
||||
{{#if this.printable}}
|
||||
<button data-tooltip="Drucken" id="printbutton">🖨</button>
|
||||
{{/if}}
|
||||
<button class="close">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-content">
|
||||
{{{contentHtml}}}
|
||||
</div>
|
||||
<div class="window-resize-handle"></div>
|
||||
</div>
|
||||
|
||||
19
public/views/partials/window.hbs
Normal file
@@ -0,0 +1,19 @@
|
||||
<div class="window" data-winid="{{appname}}-{{@index}}" data-plugin="{{appname}}">
|
||||
<div class="window-titlebar">
|
||||
<img src="{{#equaler section "==" "Plugin"}}{{appname}}{{/equaler}}/images/{{icon}}" class="window-icon" />
|
||||
<div class="title">{{appname}} [{{label}}]</div>
|
||||
<div class="controls">
|
||||
{{#if tutorial}}
|
||||
<button class="tutorial" id="tutorial-{{appname}}-{{label}}" onclick="tutorial.start()" data-tooltip="Startet eine interaktive Einführung">💡</button>
|
||||
{{/if}}
|
||||
<button class="minimize" style="transform:translateY(0px)">🗕</button>
|
||||
<button class="maximize">▢</button>
|
||||
<button class="close">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window-content">
|
||||
{{{contentHtml}}}
|
||||
</div>
|
||||
<div class="window-resize-handle"></div>
|
||||
</div>
|
||||
|
||||
242
public/views/plugindashboard.hbs
Normal file
@@ -0,0 +1,242 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Plugin Dashboard</title>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="pluginTabs" class="tabs"></div>
|
||||
<div class="card static">
|
||||
<div class="tab-contents container static" id="pluginsTabWrapper" style="height:100vh;grid-template-columns: repeat(3, 1fr);"></div>
|
||||
</div>
|
||||
<div class="grid" style="pointer-events:auto;position:absolute; bottom:18px;right:18px;grid-template-columns: 1fr;">
|
||||
<button class="bluebutton" id="createNewPlugin">Neues Plugin</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
createNewPlugin.onclick = () => {
|
||||
feedbox({
|
||||
title: 'Neues Plugin erstellen',
|
||||
message: `
|
||||
<input id="newPluginName" placeholder="Name des Plugins" />
|
||||
`,
|
||||
buttons: {
|
||||
yes: {
|
||||
text: '<b>Erstellen</b>',
|
||||
onClick: () => {
|
||||
const name = document.getElementById('newPluginName').value;
|
||||
pluginAPI.create(name);
|
||||
}
|
||||
},
|
||||
no: {
|
||||
text: 'Abbrechen',
|
||||
onClick: () => { /* automatisch geschlossen */ }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
{"name":"Demo",
|
||||
"description":"Beschreibung hier einfügen",
|
||||
"version":"1.0.0.0",
|
||||
"menu":{
|
||||
"label":"Demo",
|
||||
"items":[
|
||||
{"label":"Demo",
|
||||
"view":"index",
|
||||
"defaultSize":{"width":"600px","height":"150px"},
|
||||
"icon":"app.png",
|
||||
"permissions":["*"]}
|
||||
]},
|
||||
"config":{},
|
||||
"active":true
|
||||
}
|
||||
}*/
|
||||
|
||||
fetch('/api/plugins/getAll', { method: 'POST' })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
data.forEach(async metaData => {
|
||||
createTab(pluginTabs, metaData.name, async (tabElement) => {
|
||||
pluginsTabWrapper.innerHTML = '';
|
||||
|
||||
const objectsContainer = document.createElement('div');
|
||||
objectsContainer.className = 'card grid';
|
||||
objectsContainer.style = ``;
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card static row';
|
||||
card.style = `gap:0 10px;min-height:fit-content;justify-content: center;`;
|
||||
pluginsTabWrapper.appendChild(card);
|
||||
|
||||
const sortedKeys = Object.keys(metaData).sort((a, b) => {
|
||||
if (typeof metaData[a] === 'object' && typeof metaData[b] !== 'object') {
|
||||
return 1;
|
||||
} else if (typeof metaData[a] !== 'object' && typeof metaData[b] === 'object') {
|
||||
return -1;
|
||||
} else {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
});
|
||||
|
||||
for (const key of sortedKeys) { // each plugin-metaData
|
||||
if (metaData.hasOwnProperty(key) && metaData[key] !== null && !key.includes('Path')) { // only first level
|
||||
const value = metaData[key];
|
||||
const container = document.createElement('div');
|
||||
|
||||
if(typeof value === 'string') {
|
||||
container.style = `flex:${key === 'description' ? '1 1' : '0 0'} auto;`;
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.style = `font-weight:bold`;
|
||||
label.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
container.appendChild(label);
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.style="width:100%"
|
||||
input.value = value;
|
||||
input.setAttribute('data-name', metaData.name);
|
||||
input.setAttribute('data-field', key);
|
||||
|
||||
new AttachOnBlurChange(input, async (newValue, oldValue) => {
|
||||
if(key === 'name') {
|
||||
await pluginAPI.rename(oldValue, newValue);
|
||||
return;
|
||||
}
|
||||
await pluginAPI.update(`${metaData.name}`, { [key]: newValue});
|
||||
});
|
||||
|
||||
container.appendChild(input);
|
||||
|
||||
card.appendChild(container);
|
||||
}
|
||||
if(typeof value === 'boolean') {
|
||||
container.style = `flex:0;flex-direction:column; display:flex;align-items:center;`;
|
||||
const label = document.createElement('label');
|
||||
label.style = `font-weight:bold`;
|
||||
label.textContent = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
container.appendChild(label);
|
||||
|
||||
const checkbox = document.createElement('label');
|
||||
checkbox.className = 'cb cb-modern';
|
||||
checkbox.innerHTML = `
|
||||
<input onchange="pluginAPI.activation('${metaData.name}', this.checked);" id="plugin-${metaData.name}" data-status="${metaData.name}" type="checkbox" ${metaData.active ? 'checked' : ''}>
|
||||
<span class="cb-box" aria-hidden="true"></span>
|
||||
`;
|
||||
container.appendChild(checkbox);
|
||||
|
||||
card.appendChild(container);
|
||||
}
|
||||
|
||||
if(typeof value === 'object') {
|
||||
const objectCard = document.createElement('div');
|
||||
objectCard.className = 'card';
|
||||
objectCard.style = `height:100%`;
|
||||
objectCard.innerHTML = `
|
||||
<label style="font-weight:bold">${key}</label>
|
||||
<div style="overflow:auto" id="${metaData.name}-${key}"></div>
|
||||
`;
|
||||
objectsContainer.appendChild(objectCard);
|
||||
pluginsTabWrapper.appendChild(objectsContainer);
|
||||
const menu = createJsonTree({
|
||||
container: document.getElementById(`${metaData.name}-${key}`),
|
||||
data: metaData[key],
|
||||
schema: {
|
||||
"active": { type: "boolean" }
|
||||
},
|
||||
onChange: (data) => { },
|
||||
onSave: async (data) => {
|
||||
await pluginAPI.update(metaData.name, { [key]: data });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
pluginsTabWrapper.appendChild(objectsContainer);
|
||||
|
||||
|
||||
|
||||
|
||||
/*pluginsTabWrapper.innerHTML = `
|
||||
<div class="card static row" style="grid-column: span 3;height:50px;">
|
||||
<label class="cb cb-modern">
|
||||
<label for="plugin-${plugin.name}" style="font-weight:bold">Aktiv</label>
|
||||
<input onchange="pluginAPI.activation('${plugin.name}', this.checked);" id="plugin-${plugin.name}" data-status="${plugin.name}" type="checkbox" ${plugin.active ? 'checked' : ''}>
|
||||
<span class="cb-box" aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="card static row" style="gap:0 10px;min-height:fit-content;">
|
||||
<div style="flex:0 0 auto">
|
||||
<label style="font-weight:bold">Name</label>
|
||||
<input type="text" data-name="${plugin.name}" data-field="name" value="${plugin.name}" />
|
||||
</div>
|
||||
|
||||
<div style="flex:0 0 auto">
|
||||
<label style="font-weight:bold">Version</label>
|
||||
<input type="text" data-name="${plugin.name}" data-field="version" value="${plugin.version}" />
|
||||
</div>
|
||||
|
||||
<div style="flex:1 1 auto">
|
||||
<label style="font-weight:bold">Beschreibung</label>
|
||||
<input type="text" data-name="${plugin.name}" data-field="description" style="width:100%" value="${plugin.description}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card static" style="grid-column: span 3;">
|
||||
<label style="font-weight:bold">Startmenüeinträge</label>
|
||||
<div style="overflow:auto" id="${plugin.name}-menu"></div>
|
||||
</div>
|
||||
`;
|
||||
createJsonTree({
|
||||
container: document.getElementById(`${plugin.name}-menu`),
|
||||
data: plugin,
|
||||
schema: {
|
||||
"active": { type: "boolean" }
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
});
|
||||
|
||||
/*
|
||||
{
|
||||
"name": "TEST",
|
||||
"description": "Remote Desktop Verbindungen über MSRA realisieren",
|
||||
"version": "0.2025.11.07",
|
||||
"config": {},
|
||||
"menu": {
|
||||
"label": "TEST",
|
||||
"items": [
|
||||
{
|
||||
"label": "Remoteunterstützung",
|
||||
"view": "index",
|
||||
"icon": "remoteServer.png",
|
||||
"permissions": [
|
||||
"Administration"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"active": true
|
||||
}
|
||||
*/
|
||||
/*
|
||||
new AttachOnBlurChange(document.querySelector(`[data-name="${plugin.name}"][data-field="name"]`), async (newValue, oldValue) => { await pluginAPI.rename(`${plugin.name}`, newValue); });
|
||||
new AttachOnBlurChange(document.querySelector(`[data-name="${plugin.name}"][data-field="version"]`), async (newValue, oldValue) => await pluginAPI.update(`${plugin.name}`, { version: newValue }))
|
||||
new AttachOnBlurChange(document.querySelector(`[data-name="${plugin.name}"][data-field="description"]`), async (newValue, oldValue) => await pluginAPI.update(`${plugin.name}`, { description: newValue }))
|
||||
*/
|
||||
});
|
||||
})
|
||||
</script>
|
||||
</html>
|
||||