Quelltext des aktuellen Standes
/**
* IT-Dokumentation - WYSIWYG Editor
* Sicherer, funktionsreicher Editor
*/
'use strict';
window.OpenNIT = window.OpenNIT || {};
/**
* WYSIWYG Editor Klasse
*/
OpenNIT.WysiwygEditor = function(container, options) {
this.container = typeof container === 'string' ?
document.querySelector(container) : container;
if (!this.container) {
throw new Error('Editor container not found');
}
this.options = Object.assign({
placeholder: 'Inhalt eingeben...',
uploadUrl: '/files/upload',
csrfToken: '',
onChange: null,
onImageUpload: null,
aiImproveEnabled: (typeof window.AI_EDITOR_ENABLED !== 'undefined') ? window.AI_EDITOR_ENABLED : false,
}, options);
this.toolbar = null;
this.editor = null;
this.hiddenInput = null;
this.init();
};
OpenNIT.WysiwygEditor.prototype = {
/**
* Editor initialisieren
*/
init: function() {
this.createToolbar();
this.createEditor();
this.bindEvents();
if (this.options.aiImproveEnabled) {
this.initAiButton();
}
},
/**
* Toolbar erstellen
*/
createToolbar: function() {
const toolbarHtml = `
<div class="wysiwyg-toolbar">
<div class="toolbar-group">
<select class="toolbar-select" data-command="formatBlock" title="${window.onitText('Format')}">
<option value="p">${window.onitText('Absatz')}</option>
<option value="h2">${window.onitText('Überschrift 2')}</option>
<option value="h3">${window.onitText('Überschrift 3')}</option>
<option value="h4">${window.onitText('Überschrift 4')}</option>
</select>
</div>
${this.options.fontSize ? `
<div class="toolbar-group">
<select class="toolbar-select" data-command="fontSize" title="${window.onitText('Schriftgröße')}">
<option value="1">${window.onitText('Sehr klein')}</option>
<option value="2">${window.onitText('Klein')}</option>
<option value="3" selected>${window.onitText('Normal')}</option>
<option value="4">${window.onitText('Groß')}</option>
<option value="5">${window.onitText('Größer')}</option>
<option value="6">${window.onitText('Sehr groß')}</option>
<option value="7">${window.onitText('Riesig')}</option>
</select>
</div>` : ''}
<div class="toolbar-group">
<button type="button" class="toolbar-btn" data-command="bold" title="${window.onitText('Fett (Ctrl+B)')}"><b>B</b></button>
<button type="button" class="toolbar-btn" data-command="italic" title="${window.onitText('Kursiv (Ctrl+I)')}"><i>I</i></button>
<button type="button" class="toolbar-btn" data-command="underline" title="${window.onitText('Unterstrichen (Ctrl+U)')}"><u>U</u></button>
<button type="button" class="toolbar-btn" data-command="strikeThrough" title="${window.onitText('Durchgestrichen')}"><s>S</s></button>
</div>
<div class="toolbar-group">
<button type="button" class="toolbar-btn" data-command="insertUnorderedList" title="${window.onitText('Aufzählung')}">•</button>
<button type="button" class="toolbar-btn" data-command="insertOrderedList" title="${window.onitText('Nummerierung')}">1.</button>
</div>
<div class="toolbar-group">
<button type="button" class="toolbar-btn" data-command="createLink" title="${window.onitText('Link einfügen')}">🔗</button>
<button type="button" class="toolbar-btn" data-command="insertImage" title="${window.onitText('Bild einfügen')}">📷</button>
<button type="button" class="toolbar-btn" data-command="insertTable" title="${window.onitText('Tabelle einfügen')}">▦</button>
</div>
<div class="toolbar-group">
<button type="button" class="toolbar-btn" data-command="formatCode" title="${window.onitText('Code')}"></></button>
<button type="button" class="toolbar-btn" data-command="insertCodeBlock" title="${window.onitText('Codeblock')}">☷</button>
<button type="button" class="toolbar-btn" data-command="formatQuote" title="${window.onitText('Zitat')}">“</button>
</div>
<div class="toolbar-group">
<button type="button" class="toolbar-btn" data-command="insertCallout" data-type="info" title="${window.onitText('Info-Box')}">ℹ</button>
<button type="button" class="toolbar-btn" data-command="insertCallout" data-type="warning" title="${window.onitText('Warnung')}">⚠</button>
</div>
<div class="toolbar-group">
<button type="button" class="toolbar-btn" data-command="removeFormat" title="${window.onitText('Formatierung entfernen')}">✖</button>
</div>
</div>
`;
const wrapper = document.createElement('div');
wrapper.innerHTML = toolbarHtml;
this.toolbar = wrapper.firstElementChild;
this.container.appendChild(this.toolbar);
},
/**
* Editor-Bereich erstellen
*/
createEditor: function() {
this.editor = document.createElement('div');
this.editor.className = 'wysiwyg-editor';
this.editor.contentEditable = 'true';
this.editor.dataset.placeholder = this.options.placeholder;
// Vorhandenen Inhalt übernehmen
const existingContent = this.container.querySelector('textarea, input[type="hidden"]');
if (existingContent) {
this.hiddenInput = existingContent;
this.editor.innerHTML = existingContent.value;
existingContent.style.display = 'none';
}
this.container.appendChild(this.editor);
},
/**
* Event-Handler binden
*/
bindEvents: function() {
const self = this;
// Toolbar-Buttons
this.toolbar.querySelectorAll('.toolbar-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
const command = this.dataset.command;
self.execCommand(command, this.dataset);
self.editor.focus();
});
});
// Format-Select
this.toolbar.querySelectorAll('.toolbar-select').forEach(function(select) {
select.addEventListener('change', function() {
const command = this.dataset.command;
document.execCommand(command, false, this.value);
self.editor.focus();
self.updateContent();
});
});
// Inhalt synchronisieren
this.editor.addEventListener('input', function() {
self.updateContent();
});
// Paste-Handler für Bilder
this.editor.addEventListener('paste', function(e) {
self.handlePaste(e);
});
// Drop-Handler für Bilder
this.editor.addEventListener('drop', function(e) {
self.handleDrop(e);
});
// Drag-Over
this.editor.addEventListener('dragover', function(e) {
e.preventDefault();
});
// Keyboard-Shortcuts
this.editor.addEventListener('keydown', function(e) {
self.handleKeydown(e);
});
},
/**
* Befehl ausführen
*/
execCommand: function(command, data) {
switch (command) {
case 'createLink':
this.insertLink();
break;
case 'insertImage':
this.showImageDialog();
break;
case 'insertTable':
this.showTableDialog();
break;
case 'formatCode':
this.wrapSelection('code');
break;
case 'insertCodeBlock':
this.insertCodeBlock();
break;
case 'formatQuote':
document.execCommand('formatBlock', false, 'blockquote');
break;
case 'insertCallout':
this.insertCallout(data.type || 'info');
break;
default:
document.execCommand(command, false, null);
}
this.updateContent();
},
/**
* Link einfügen
*/
insertLink: function() {
const selection = window.getSelection();
const selectedText = selection.toString();
const url = prompt('URL eingeben:', 'https://');
if (url && url !== 'https://') {
// URL validieren
if (!this.isValidUrl(url)) {
alert('Ungültige URL');
return;
}
document.execCommand('createLink', false, url);
}
},
/**
* URL validieren
*/
isValidUrl: function(string) {
try {
const url = new URL(string);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch (_) {
return false;
}
},
/**
* Bild-Dialog anzeigen
*/
showImageDialog: function() {
const self = this;
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.addEventListener('change', function() {
if (this.files && this.files[0]) {
self.uploadImage(this.files[0]);
}
});
input.click();
},
/**
* Bild hochladen
*/
uploadImage: function(file) {
const self = this;
if (!file.type.startsWith('image/')) {
alert('Bitte nur Bilder hochladen');
return;
}
// Max 10MB
if (file.size > 10 * 1024 * 1024) {
alert(window.onitText('Datei zu groß (max. 10MB)'));
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('_csrf', this.options.csrfToken);
// Placeholder anzeigen
const placeholder = document.createElement('span');
placeholder.className = 'image-uploading';
placeholder.textContent = 'Bild wird hochgeladen...';
this.insertNodeAtCursor(placeholder);
fetch(this.options.uploadUrl, {
method: 'POST',
body: formData
})
.then(function(response) {
return response.json();
})
.then(function(data) {
if (data.success && data.url) {
const img = document.createElement('img');
img.src = data.url;
img.alt = file.name;
placeholder.replaceWith(img);
self.updateContent();
} else {
placeholder.remove();
alert(data.error || 'Upload fehlgeschlagen');
}
})
.catch(function() {
placeholder.remove();
alert('Upload fehlgeschlagen');
});
},
/**
* Paste-Handler
*/
handlePaste: function(e) {
const self = this;
const items = e.clipboardData?.items;
if (items) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
e.preventDefault();
const file = items[i].getAsFile();
this.uploadImage(file);
return;
}
}
}
// HTML-Paste bereinigen
if (e.clipboardData?.types.includes('text/html')) {
e.preventDefault();
const html = e.clipboardData.getData('text/html');
const cleanHtml = this.sanitizeHtml(html);
document.execCommand('insertHTML', false, cleanHtml);
}
},
/**
* Drop-Handler
*/
handleDrop: function(e) {
e.preventDefault();
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
for (let i = 0; i < files.length; i++) {
if (files[i].type.startsWith('image/')) {
this.uploadImage(files[i]);
}
}
}
},
/**
* Keyboard-Handler
*/
handleKeydown: function(e) {
// Tab in Code-Block
if (e.key === 'Tab') {
const selection = window.getSelection();
const node = selection.anchorNode?.parentElement;
if (node && (node.tagName === 'CODE' || node.closest('pre'))) {
e.preventDefault();
document.execCommand('insertText', false, ' ');
}
}
// Ctrl+K für Link
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
this.insertLink();
}
},
/**
* Tabellen-Dialog
*/
showTableDialog: function() {
const rows = prompt('Anzahl Zeilen:', '3');
const cols = prompt('Anzahl Spalten:', '3');
if (rows && cols) {
this.insertTable(parseInt(rows), parseInt(cols));
}
},
/**
* Tabelle einfügen
*/
insertTable: function(rows, cols) {
let html = '<table><thead><tr>';
for (let c = 0; c < cols; c++) {
html += '<th>Spalte ' + (c + 1) + '</th>';
}
html += '</tr></thead><tbody>';
for (let r = 0; r < rows; r++) {
html += '<tr>';
for (let c = 0; c < cols; c++) {
html += '<td> </td>';
}
html += '</tr>';
}
html += '</tbody></table><p> </p>';
document.execCommand('insertHTML', false, html);
this.updateContent();
},
/**
* Code-Block einfügen
*/
insertCodeBlock: function() {
const language = prompt('Programmiersprache (optional):', '');
const html = '<pre><code class="language-' + (language || 'plaintext') + '">// Code hier eingeben</code></pre><p> </p>';
document.execCommand('insertHTML', false, html);
this.updateContent();
},
/**
* Callout-Box einfügen
*/
insertCallout: function(type) {
const titles = {
info: 'Hinweis',
warning: 'Warnung',
danger: 'Achtung',
success: 'Erfolg'
};
const html = '<div class="callout callout-' + type + '">' +
'<div class="callout-title">' + titles[type] + '</div>' +
'<p>Text hier eingeben...</p>' +
'</div><p> </p>';
document.execCommand('insertHTML', false, html);
this.updateContent();
},
/**
* Selection in Element wrappen
*/
wrapSelection: function(tagName) {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const element = document.createElement(tagName);
range.surroundContents(element);
this.updateContent();
}
},
/**
* Node an Cursor einfügen
*/
insertNodeAtCursor: function(node) {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
},
/**
* HTML bereinigen
*/
sanitizeHtml: function(html) {
// Einfache Bereinigung - Server macht die eigentliche Arbeit
const div = document.createElement('div');
div.innerHTML = html;
// Script-Tags entfernen
div.querySelectorAll('script, style, link, meta').forEach(function(el) {
el.remove();
});
// Event-Handler entfernen
div.querySelectorAll('*').forEach(function(el) {
Array.from(el.attributes).forEach(function(attr) {
if (attr.name.startsWith('on') || attr.name === 'style') {
el.removeAttribute(attr.name);
}
});
});
return div.innerHTML;
},
/**
* Inhalt aktualisieren
*/
updateContent: function() {
if (this.hiddenInput) {
this.hiddenInput.value = this.editor.innerHTML;
}
if (typeof this.options.onChange === 'function') {
this.options.onChange(this.getContent());
}
},
/**
* Inhalt abrufen
*/
getContent: function() {
return this.editor.innerHTML;
},
/**
* Inhalt setzen
*/
setContent: function(html) {
this.editor.innerHTML = html;
this.updateContent();
},
/**
* Text abrufen
*/
getText: function() {
return this.editor.textContent;
},
/**
* Fokus setzen
*/
focus: function() {
this.editor.focus();
},
// ── KI-Textverbesserung ─────────────────────────────────────────────────
/**
* KI-Button und Popup zur Toolbar hinzufügen
*/
initAiButton: function() {
var self = this;
// Toolbar-Gruppe mit Button
var group = document.createElement('div');
group.className = 'toolbar-group';
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'toolbar-btn toolbar-btn-ai';
btn.title = 'Text mit KI verbessern';
btn.innerHTML = '✨ KI';
group.appendChild(btn);
this.toolbar.appendChild(group);
// Popup erstellen und an body hängen (fixed, damit overflow:hidden nicht stört)
this.aiPopup = this._createAiPopup();
document.body.appendChild(this.aiPopup);
this.savedRange = null;
this.savedIsFullContent = false;
btn.addEventListener('click', function(e) {
e.stopPropagation();
var sel = window.getSelection();
if (sel && sel.rangeCount > 0 && sel.toString().trim() !== '') {
self.savedRange = sel.getRangeAt(0).cloneRange();
self.savedIsFullContent = false;
} else {
self.savedRange = null;
self.savedIsFullContent = true;
}
var visible = self.aiPopup.style.display !== 'none' && self.aiPopup.style.display !== '';
if (visible) {
self.aiPopup.style.display = 'none';
} else {
// Position berechnen: rechts neben / unterhalb des Buttons, immer sichtbar
var rect = btn.getBoundingClientRect();
var popupW = 330;
var margin = 8;
var left = rect.right - popupW;
if (left < margin) left = margin;
var top = rect.bottom + margin;
// Popup-Höhe schätzen (~340px); wenn zu weit unten → über Button öffnen
if (top + 340 > window.innerHeight - margin) {
top = Math.max(margin, rect.top - 340 - margin);
}
self.aiPopup.style.left = left + 'px';
self.aiPopup.style.top = top + 'px';
self.aiPopup.style.display = 'block';
self.aiPopup.querySelector('.ai-instruction-input').focus();
}
});
// Klick außerhalb schließt Popup
document.addEventListener('click', function(e) {
if (self.aiPopup && !self.aiPopup.contains(e.target) && e.target !== btn) {
self.aiPopup.style.display = 'none';
}
});
},
/**
* Popup-Element aufbauen
*/
_createAiPopup: function() {
var self = this;
var presets = [
{ label: window.onitText('Freundlicher'), value: 'Schreibe den Text freundlicher und zugänglicher um' },
{ label: window.onitText('Professioneller'), value: 'Schreibe den Text professioneller und formeller um' },
{ label: window.onitText('Kürzer'), value: 'Kürze den Text auf das Wesentliche, ohne Informationen zu verlieren' },
{ label: window.onitText('Ausführlicher'), value: 'Ergänze den Text sinnvoll mit hilfreichen Details' },
{ label: window.onitText('Fehler korrigieren'), value: 'Korrigiere alle Rechtschreib- und Grammatikfehler' },
];
var presetHtml = presets.map(function(p) {
return '<button type="button" class="ai-preset-btn" data-value="' + p.value + '">' + p.label + '</button>';
}).join('');
var popup = document.createElement('div');
popup.className = 'wysiwyg-ai-popup';
popup.style.display = 'none';
popup.innerHTML =
'<div class="wysiwyg-ai-popup-header">' +
'<span>✨ KI-Textverbesserung</span>' +
'<button type="button" class="ai-popup-close" title="Schließen">✕</button>' +
'</div>' +
'<p class="wysiwyg-ai-hint">Markierten Text oder gesamten Inhalt verbessern:</p>' +
'<div class="wysiwyg-ai-presets">' + presetHtml + '</div>' +
'<div class="wysiwyg-ai-input-row">' +
'<input type="text" class="ai-instruction-input" placeholder="Eigene Anweisung eingeben\u2026" maxlength="300">' +
'<button type="button" class="ai-apply-btn">Anwenden</button>' +
'</div>' +
'<div class="wysiwyg-ai-status" style="display:none"></div>';
popup.querySelector('.ai-popup-close').addEventListener('click', function() {
popup.style.display = 'none';
});
popup.querySelectorAll('.ai-preset-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
popup.querySelector('.ai-instruction-input').value = this.dataset.value;
self._applyAiImprovement();
});
});
popup.querySelector('.ai-apply-btn').addEventListener('click', function() {
self._applyAiImprovement();
});
popup.querySelector('.ai-instruction-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); self._applyAiImprovement(); }
});
// Klick im Popup nicht an document weiterleiten
popup.addEventListener('click', function(e) { e.stopPropagation(); });
return popup;
},
/**
* KI-Verbesserung anwenden
*/
_applyAiImprovement: function() {
var self = this;
var instruction = this.aiPopup.querySelector('.ai-instruction-input').value.trim();
var applyBtn = this.aiPopup.querySelector('.ai-apply-btn');
if (!instruction) {
this._showAiStatus('Bitte eine Anweisung eingeben.', 'error');
return;
}
// Text ermitteln: Selektion oder voller Inhalt
var text = '';
if (!this.savedIsFullContent && this.savedRange) {
text = this.savedRange.toString().trim();
}
if (!text) {
text = this.getText().trim();
this.savedIsFullContent = true;
}
if (!text) {
this._showAiStatus('Kein Text zum Verbessern vorhanden.', 'error');
return;
}
var csrf = this.options.csrfToken ||
((document.querySelector('input[name="_csrf"]') || {}).value || '');
applyBtn.disabled = true;
this._showAiStatus('KI verarbeitet Text\u2026', 'loading');
var body = 'text=' + encodeURIComponent(text) +
'&instruction=' + encodeURIComponent(instruction) +
'&_csrf=' + encodeURIComponent(csrf);
fetch('/api/ai-chat/improve-text', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
},
body: body,
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
self._showAiStatus(data.error, 'error');
return;
}
var improved = (data.improved || '').trim();
if (!improved) {
self._showAiStatus('Keine Antwort erhalten.', 'error');
return;
}
// Ergebnis einfügen
self.editor.focus();
if (!self.savedIsFullContent && self.savedRange) {
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(self.savedRange);
document.execCommand('insertText', false, improved);
} else {
var html = '<p>' + improved
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\n\n+/g, '</p><p>')
.replace(/\n/g, '<br>') + '</p>';
self.setContent(html);
}
self._showAiStatus('✓ Text erfolgreich verbessert', 'success');
setTimeout(function() { self.aiPopup.style.display = 'none'; }, 1600);
})
.catch(function() {
self._showAiStatus('Netzwerkfehler \u2013 bitte erneut versuchen.', 'error');
})
.finally(function() {
applyBtn.disabled = false;
});
},
/**
* Status-Meldung im Popup anzeigen
*/
_showAiStatus: function(msg, type) {
var el = this.aiPopup.querySelector('.wysiwyg-ai-status');
el.innerHTML = msg;
el.className = 'wysiwyg-ai-status wysiwyg-ai-status--' + (type || 'info');
el.style.display = 'block';
}
};
/**
* Editor initialisieren (Helper)
*/
OpenNIT.initWysiwyg = function(selector, options) {
const containers = document.querySelectorAll(selector);
const editors = [];
containers.forEach(function(container) {
editors.push(new OpenNIT.WysiwygEditor(container, options));
});
return editors.length === 1 ? editors[0] : editors;
};