diff --git a/config/current.ver b/config/current.ver index c3635d3..66aafdf 100644 --- a/config/current.ver +++ b/config/current.ver @@ -1 +1 @@ -1.1.55 \ No newline at end of file +1.1.56 \ No newline at end of file diff --git a/public/assets/js/bridge/rte-editor.js b/public/assets/js/bridge/rte-editor.js index f116717..4cb8dd2 100644 --- a/public/assets/js/bridge/rte-editor.js +++ b/public/assets/js/bridge/rte-editor.js @@ -480,6 +480,17 @@ return null; } }; + const findLinkAtSelection = () => { + const range = getSelectionRange(); + if (!range) return null; + let node = range.commonAncestorContainer; + if (node && node.nodeType === 3) node = node.parentNode; + while (node && node !== content) { + if (node.tagName && node.tagName.toUpperCase() === 'A') return node; + node = node.parentNode; + } + return null; + }; const applyInlineStyle = (styleProp, value) => { try { content.focus(); @@ -523,6 +534,22 @@ for (const child of Array.from(fragment.childNodes)) { if (walker(child)) return true; } + const checkAncestors = (node) => { + let cur = node; + while (cur && cur !== content) { + if (cur.nodeType === 1) { + const tag = cur.tagName ? cur.tagName.toUpperCase() : ''; + if (tags && tags.includes(tag)) return true; + if (styleProp && cur.style) { + if (styleMatch && styleMatch(cur.style[styleProp] || '', cur)) return true; + } + } + cur = cur.parentNode; + } + return false; + }; + if (checkAncestors(range.startContainer)) return true; + if (checkAncestors(range.endContainer)) return true; } catch {} return false; }; @@ -655,7 +682,8 @@ return; } const value = typeof valueGetter === 'function' ? valueGetter() : valueGetter; - if (value === null || value === undefined) return; + const hasValue = typeof valueGetter !== 'undefined'; + if (hasValue && (value === null || value === undefined)) return; if (cmd === 'createLink' && !value) return; exec(cmd, value); }); @@ -803,8 +831,103 @@ addButton(icon('M5 7h14v2H5zM4 11h16v2H4zM5 15h14v2H5z'), 'Zentriert', 'justifyCenter'); addButton(icon('M10 7h10v2H10zM4 11h16v2H4zM8 15h12v2H8z'), 'Rechtsbuendig', 'justifyRight'); addButton(icon('M4 7h16v2H4zM4 11h16v2H4zM4 15h16v2H4z'), 'Blocksatz', 'justifyFull'); - addButton(icon('M7 7h10l-1.5 1.5-3-3-5.5 5.5v5h5l5.5-5.5-3-3L17 7z'), 'Link einfuegen', 'createLink', () => prompt('Link-URL eingeben', 'https://')); - addButton(icon('M7 7h4v2H7v4H5V9a2 2 0 0 1 2-2zm10 0a2 2 0 0 1 2 2v4h-2V9h-4V7h4zm0 10h-4v-2h4v-4h2v4a2 2 0 0 1-2 2zm-10 0a2 2 0 0 1-2-2v-4h2v4h4v2H7z'), 'Link entfernen', 'unlink'); + const linkPanel = doc.createElement('div'); + linkPanel.style.display = 'none'; + linkPanel.style.alignItems = 'center'; + linkPanel.style.gap = '6px'; + linkPanel.style.padding = '6px'; + linkPanel.style.border = '1px solid #cbd5f5'; + linkPanel.style.borderRadius = '6px'; + linkPanel.style.background = '#ffffff'; + linkPanel.style.boxShadow = '0 2px 10px rgba(15,23,42,0.08)'; + linkPanel.className = 'bridge-rte-link-panel'; + linkPanel.setAttribute('data-bridge-rte', 'link-panel'); + + const linkInput = doc.createElement('input'); + linkInput.type = 'text'; + linkInput.placeholder = 'https://...'; + linkInput.style.flex = '1'; + linkInput.style.minWidth = '220px'; + linkInput.style.padding = '6px 8px'; + linkInput.style.border = '1px solid #cbd5f5'; + linkInput.style.borderRadius = '4px'; + linkInput.style.fontSize = '14px'; + + const linkApply = doc.createElement('button'); + linkApply.type = 'button'; + linkApply.textContent = 'OK'; + linkApply.className = 'bridge-rte-btn'; + linkApply.style.padding = '6px 10px'; + + const linkRemove = doc.createElement('button'); + linkRemove.type = 'button'; + linkRemove.textContent = 'Entfernen'; + linkRemove.className = 'bridge-rte-btn'; + linkRemove.style.padding = '6px 10px'; + + const linkCancel = doc.createElement('button'); + linkCancel.type = 'button'; + linkCancel.textContent = 'Abbrechen'; + linkCancel.className = 'bridge-rte-btn'; + linkCancel.style.padding = '6px 10px'; + + linkPanel.appendChild(linkInput); + linkPanel.appendChild(linkApply); + linkPanel.appendChild(linkRemove); + linkPanel.appendChild(linkCancel); + toolbar.appendChild(linkPanel); + + const hideLinkPanel = () => { + linkPanel.style.display = 'none'; + }; + const showLinkPanel = () => { + const linkEl = findLinkAtSelection(); + linkInput.value = linkEl && linkEl.getAttribute ? (linkEl.getAttribute('href') || '') : ''; + linkPanel.style.display = 'flex'; + linkInput.focus(); + linkInput.select(); + }; + + addButton( + icon('M7 7h10l-1.5 1.5-3-3-5.5 5.5v5h5l5.5-5.5-3-3L17 7z'), + 'Link einfuegen/bearbeiten', + null, + null, + () => { + saveSelection(); + showLinkPanel(); + } + ); + + linkApply.addEventListener('click', () => { + const url = String(linkInput.value || '').trim(); + if (!url) { + exec('unlink'); + hideLinkPanel(); + return; + } + try { + const linkEl = findLinkAtSelection(); + if (linkEl) { + const docRef = content.ownerDocument || document; + const sel = docRef.getSelection(); + if (sel && docRef.createRange) { + const range = docRef.createRange(); + range.selectNodeContents(linkEl); + sel.removeAllRanges(); + sel.addRange(range); + saveSelection(); + } + } + } catch {} + exec('createLink', url); + hideLinkPanel(); + }); + linkRemove.addEventListener('click', () => { + exec('unlink'); + hideLinkPanel(); + }); + linkCancel.addEventListener('click', hideLinkPanel); addButton(icon('M7 6h10v2H7zM9 10h6v2H9zM10 14h4v2h-4z'), 'Tiefgestellt', 'subscript'); addButton(icon('M7 6h10v2H7zM9 10h6v2H9zM8 14h8v2H8z'), 'Hochgestellt', 'superscript'); addButton(icon('M7 4h4v4H7V4zm6 12h4v4h-4v-4zM7 10h10v2H7v-2z'), 'Einzug', 'indent');