feat: add new-tab links and removable media previews (1.7.3)
This commit is contained in:
1 parent
b8991ddd5c
commit
6d21c452bd
28 files changed
+496
-46
No files matched your search
@@ -708,6 +708,7 @@ details.card > summary:hover { background: #fafbf8 }
|
||||
|
||||
/* Search and forms share usable widths throughout administration. */
|
||||
.field-wide{grid-column:1/-1;min-width:0}.field-wide textarea{width:100%;min-height:100px;resize:vertical}
|
||||
.selected-media-preview{margin:12px 0;padding:10px;border:1px solid var(--line);border-radius:9px;background:#fff;min-width:0}.selected-media-preview img{display:block;width:100%;height:140px;object-fit:contain;background:repeating-conic-gradient(#eef1eb 0% 25%,#fff 0% 50%) 0 0/16px 16px}.selected-media-preview figcaption{font-size:12px;line-height:1.5;overflow-wrap:anywhere;margin-top:8px;color:var(--muted)}
|
||||
.live-search-ready .search-submit{display:none}.search-status{min-height:20px;margin:-12px 0 0}#list-results{scroll-margin-top:16px}#list-results>.section-head{margin-top:0}#list-results:focus-visible{outline:2px solid var(--accent);outline-offset:5px}#search-error p{margin-top:0}
|
||||
.list-toolbar{display:flex;flex-wrap:wrap;align-items:end;gap:14px;padding:18px;margin:24px 0;background:var(--paper,#fff);border:1px solid var(--line,#dce3da);border-radius:12px}
|
||||
.list-toolbar>div{min-width:140px}.list-toolbar .list-search{flex:1;min-width:220px}.list-toolbar input,.list-toolbar select{width:100%}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* appear here without touching this file.
|
||||
*/
|
||||
import { devices, effectiveStyle, hoverStyle, setStyle, resetStyleDevice, clearStyle, isContainer, relevantProperty } from './editor-model.js';
|
||||
import { openMedia } from './media.js';
|
||||
import { openMedia, createMediaPreview } from './media.js';
|
||||
|
||||
export const GROUPS = {
|
||||
text: 'Typografie',
|
||||
@@ -444,15 +444,18 @@ export function createDesignPanel({ schema, onChange, onSavePreset, getBlock, ge
|
||||
|
||||
const mediaControl = (key, value, apply) => {
|
||||
const wrap = element('div', 'media-control');
|
||||
const status = element('p', 'media-chosen', value ? `Bild gewählt (${String(value).slice(0, 8)}…)` : 'Kein Bild gewählt');
|
||||
const choose = element('button', 'secondary', value ? 'Anderes Bild wählen' : 'Bild aus der Mediathek');
|
||||
const hasImage = Boolean(value && value !== 'none');
|
||||
const preview = createMediaPreview({ mediaId: hasImage ? value : '' });
|
||||
const choose = element('button', 'secondary', hasImage ? 'Anderes Bild wählen' : 'Bild aus der Mediathek');
|
||||
choose.type = 'button';
|
||||
choose.id = `style-${key}`; choose.setAttribute('aria-label', choose.textContent);
|
||||
choose.addEventListener('click', () => openMedia({ onChoose: items => { if (items.length) apply(items[0].id); } }));
|
||||
wrap.append(status, choose);
|
||||
if (value) {
|
||||
wrap.append(preview.element, choose);
|
||||
if (hasImage) {
|
||||
const remove = element('button', 'text-button danger-text', 'Bild entfernen');
|
||||
remove.type = 'button';
|
||||
remove.addEventListener('click', () => apply(''));
|
||||
remove.title = 'Hintergrundbild für dieses Gerät entfernen. Die Datei bleibt in der Mediathek.';
|
||||
remove.addEventListener('click', () => { apply('none'); document.getElementById(choose.id)?.focus(); });
|
||||
wrap.append(remove);
|
||||
}
|
||||
return wrap;
|
||||
@@ -541,7 +544,7 @@ export function createDesignPanel({ schema, onChange, onSavePreset, getBlock, ge
|
||||
else if (property.type === 'number') control = numberControl(id, own ? current.value : '', property, apply);
|
||||
else if (property.type === 'choice') control = choiceControl(id, own ? current.value : '', property, apply, inheritedText);
|
||||
else if (property.type === 'font') control = fontControl(id, own ? current.value : '', apply, inheritedText);
|
||||
else if (property.type === 'media') control = mediaControl(key, own ? current.value : '', apply);
|
||||
else if (property.type === 'media') control = mediaControl(key, current.value || '', apply);
|
||||
else continue;
|
||||
|
||||
body.append(row(id, property.label, control, { own, inherited: inheritedText, onReset, chips: deviceChips(block, key, property.label, false) }));
|
||||
@@ -618,6 +621,7 @@ export function createDesignPanel({ schema, onChange, onSavePreset, getBlock, ge
|
||||
};
|
||||
|
||||
function describe(property, value) {
|
||||
if (property.type === 'media') return value === 'none' ? 'Kein Bild' : 'Bild aus der Mediathek';
|
||||
if (property.type === 'choice') return property.values[value] ?? value;
|
||||
return String(value);
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ export const devices = { desktop: 'Desktop', tablet: 'Tablet', mobile: 'Mobil' }
|
||||
// blieb bei 12 stehen, während Bausteine schon v13 bis v15 verlangten: in einem aktuellen Dokument
|
||||
// liess sich dann gar nichts mehr einfügen, und ein Symbol, ein Slider oder ein externer Inhalt
|
||||
// nirgends. Eine Zahl, an einer Stelle.
|
||||
export const SCHEMA_VERSION = 19;
|
||||
export const SCHEMA_VERSION = 20;
|
||||
const known = version => Number.isInteger(version) && version >= 1 && version <= SCHEMA_VERSION;
|
||||
|
||||
export function upgradeDocument(document, version = 2) {
|
||||
@@ -310,7 +310,7 @@ export function mediaProps(media) {
|
||||
export function insertMedia(document, selected, items) {
|
||||
const target = locate(document.blocks, selected)?.block;
|
||||
upgradeDocument(document, 3);
|
||||
if (target?.type === 'image') { target.props = mediaProps(items[0]); return target.id; }
|
||||
if (target?.type === 'image') { target.props = { ...target.props, ...mediaProps(items[0]), caption: target.props.caption || '' }; return target.id; }
|
||||
let parent = target;
|
||||
if (target?.type !== 'gallery') {
|
||||
const id = insertBlock(document, 'gallery', selected); parent = locate(document.blocks, id).block;
|
||||
@@ -496,6 +496,7 @@ export function setStyle(document, id, device, key, value, { side = null, hover
|
||||
if (!block) return;
|
||||
const empty = value === '' || value === null || value === undefined;
|
||||
if (!empty) upgradeDocument(document, 10);
|
||||
if (key === 'backgroundImage' && value === 'none') upgradeDocument(document, 20);
|
||||
|
||||
block.style ??= {};
|
||||
block.style[device] ??= {};
|
||||
|
||||
@@ -193,6 +193,7 @@ dialog p{font-size:14px;line-height:1.7;color:var(--muted)}
|
||||
.rt-toggle[aria-pressed=true]{background:var(--ink);color:#fff}
|
||||
.rt-link{display:flex;gap:6px;padding:6px;border-bottom:1px solid var(--line);background:#fff6e2}
|
||||
.rt-link input{font-size:11px;min-height:30px}
|
||||
.rt-link{flex-wrap:wrap;align-items:center}.rt-link>input{flex:1 0 100%;min-width:0}.rt-link .check-label{margin:0;font-size:12px}.rt-link input[type=checkbox]{min-height:0;width:auto}.selected-media-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.selected-media-list .selected-media-preview img{height:90px}.selected-media-list .text-button{font-size:11px;padding:6px}
|
||||
.rt-surface{min-height:120px;max-height:340px;overflow:auto;padding:11px 12px;font-size:13px;line-height:1.65}
|
||||
.rt-surface:focus-visible{outline:2px solid #b55b12;outline-offset:-2px}
|
||||
.rt-surface ul,.rt-surface ol{margin:0 0 8px;padding-left:20px}
|
||||
|
||||
+27
-2
@@ -1,5 +1,5 @@
|
||||
import { EditorHistory, labels, blockLabel, blockIcon, isPluginBlock, setStyle, effectiveStyle, locate, clone, isContainer, canContain, insertBlock, insertBlockAt, insertionTarget, moveBlockTo, duplicateBlock, moveBlock, devices, effectiveDesign, setDesign, blockPath, insertPreset, insertRow, insertRowAt, setEffect, insertMedia, upgradeDocument, CATEGORIES, ROW_COLUMNS, whyNot, dropList } from './editor-model.js';
|
||||
import { openMedia } from './media.js';
|
||||
import { openMedia, createMediaPreview } from './media.js';
|
||||
import { createDesignPanel } from './design-panel.js';
|
||||
import { createRichText } from './rich-text.js';
|
||||
import { createCanvasOverlay } from './canvas-overlay.js';
|
||||
@@ -177,6 +177,7 @@ function renderProperties() {
|
||||
}
|
||||
if (block && propertyPanel === 'advanced') { renderAdvanced(block, panel); return; }
|
||||
if (block && propertyPanel === 'design') { renderDesign(block, panel); return; }
|
||||
let imagePreview;
|
||||
const field = (label, key, value, options = {}) => {
|
||||
const id = `property-${key}`;
|
||||
const labelEl = document.createElement('label'); labelEl.htmlFor = id; labelEl.textContent = label;
|
||||
@@ -203,14 +204,17 @@ function renderProperties() {
|
||||
else current[key] = next;
|
||||
if (key === 'caption') upgradeDocument(doc, 3);
|
||||
if (key === 'click' || key === 'lightbox') upgradeDocument(doc, 19);
|
||||
if (key === 'newTab') upgradeDocument(doc, 20);
|
||||
if (current.type === 'image' && key === 'src') { delete current.props.sources; delete current.props.mediaId; }
|
||||
}, `${selected || 'page'}:${key}`);
|
||||
if (key === 'decorative') renderProperties();
|
||||
if (key === 'src') imagePreview?.update({ src: input.value });
|
||||
if (['decorative', 'click'].includes(key)) { renderProperties(); document.getElementById(id)?.focus({ preventScroll: true }); }
|
||||
renderTree(); controls(); schedulePreview();
|
||||
});
|
||||
input.addEventListener('blur', () => { history.mergeKey = null; });
|
||||
if (options.checkbox) panel.append(labelEl); else panel.append(labelEl, input);
|
||||
if (options.help) { const help = document.createElement('p'); help.className = 'field-help'; help.id = `${id}-help`; help.textContent = options.help; input.setAttribute('aria-describedby', help.id); panel.append(help); }
|
||||
if (key === 'href') field('In neuem Tab öffnen', 'newTab', block.props.newTab === true, { checkbox: true });
|
||||
};
|
||||
if (!block) {
|
||||
field('Seitentitel', 'title', history.document.title, { maxLength: 160 });
|
||||
@@ -334,10 +338,15 @@ function renderProperties() {
|
||||
if (block.type === 'button') field('Linkziel', 'href', block.props.href, { help: 'Lokaler Pfad (/kontakt), Sprungmarke (#abschnitt) oder HTTPS-Adresse.' });
|
||||
if (block.type === 'image') {
|
||||
renderImageWidth(block, panel);
|
||||
imagePreview = createMediaPreview({ src: block.props.sources?.[0]?.src || block.props.src, label: block.props.alt || block.props.caption });
|
||||
panel.append(imagePreview.element);
|
||||
field('Beim Klick', 'click', block.props?.click || (block.props?.href ? 'link' : 'keine'), { choices: { keine: 'Nichts tun', vergroessern: 'Bild vergrößern', link: 'Zu einer Adresse führen' }, help: 'Vergrößern öffnet das Bild über der Seite. Ohne JavaScript öffnet derselbe Klick die große Bilddatei – die Seite bleibt also in jedem Fall benutzbar.' });
|
||||
if ((block.props?.click || (block.props?.href ? 'link' : 'keine')) === 'link') field('Linkziel', 'href', block.props.href || '', { help: 'Lokaler Pfad (/kontakt), Sprungmarke (#abschnitt) oder HTTPS-Adresse. Für ein Logo im Kopfbereich meist /.' });
|
||||
const picker = document.createElement('button'); picker.className = 'secondary'; picker.textContent = 'Bild aus Mediathek wählen';
|
||||
picker.addEventListener('click', () => chooseMedia(false)); panel.append(picker);
|
||||
const remove = document.createElement('button'); remove.type = 'button'; remove.className = 'text-button danger-text'; remove.textContent = 'Bild entfernen';
|
||||
remove.title = 'Bild aus diesem Inhalt entfernen. Die Datei bleibt in der Mediathek.';
|
||||
remove.addEventListener('click', () => { actions.delete(block.id); $('#undo').focus(); }); panel.append(remove);
|
||||
field('Bildadresse', 'src', block.props.src, { help: 'Lokale Datei oder HTTPS-Adresse. Über die Mediathek wird die passende Bildvariante automatisch gewählt.' });
|
||||
field('Dekoratives Bild', 'decorative', block.props.decorative, { checkbox: true });
|
||||
if (!block.props.decorative) field('Alternativtext', 'alt', block.props.alt, { multiline: true });
|
||||
@@ -351,6 +360,22 @@ function renderProperties() {
|
||||
const picker = document.createElement('button'); picker.className = 'primary'; picker.textContent = 'Galeriebilder auswählen';
|
||||
picker.addEventListener('click', () => chooseMedia(true)); panel.append(hint, picker);
|
||||
}
|
||||
if (['gallery', 'logos'].includes(block.type)) {
|
||||
const images = document.createElement('div'); images.className = 'selected-media-list';
|
||||
for (const child of block.children || []) {
|
||||
const item = document.createElement('div');
|
||||
const title = child.props.alt || child.props.caption || 'Bild';
|
||||
item.append(createMediaPreview({ src: child.props.sources?.[0]?.src || child.props.src, label: title }).element);
|
||||
const edit = document.createElement('button'); edit.type = 'button'; edit.className = 'text-button'; edit.textContent = 'Bearbeiten'; edit.setAttribute('aria-label', `${title} bearbeiten`);
|
||||
edit.addEventListener('click', () => choose(child.id));
|
||||
const remove = document.createElement('button'); remove.type = 'button'; remove.className = 'text-button danger-text'; remove.textContent = 'Entfernen'; remove.setAttribute('aria-label', `${title} entfernen`);
|
||||
remove.addEventListener('click', () => {
|
||||
history.change(doc => { const found = locate(doc.blocks, child.id); if (found) found.siblings.splice(found.index, 1); }); changed(); $('#undo').focus();
|
||||
});
|
||||
item.append(edit, remove); images.append(item);
|
||||
}
|
||||
panel.append(images);
|
||||
}
|
||||
if (block.type === 'section') {
|
||||
field('Farbwelt', 'tone', block.layout?.tone || 'paper', { layout: true, choices: { paper: 'Hell · Papier', mint: 'Hellgrün · Akzent', ink: 'Dunkel · Tinte' } });
|
||||
field('Inhaltsbreite', 'width', block.layout?.width || 'normal', { layout: true, choices: { normal: 'Websitebreite (aus dem Theme)', narrow: 'Schmal (800 px)', wide: 'Breit (1600 px)', full: 'Randlos (volle Breite)' }, help: 'Randlos nutzt die gesamte Sektion. Text und Überschriften füllen ihren Container; eine eigene Begrenzung stellst du unter Design → Größe ein.' });
|
||||
|
||||
@@ -1,5 +1,45 @@
|
||||
const element = (tag, text, className) => { const node = document.createElement(tag); if (text) node.textContent = text; if (className) node.className = className; return node; };
|
||||
|
||||
const previews = new Map();
|
||||
async function previewItem(id) {
|
||||
if (!previews.has(id)) previews.set(id, fetch(document.querySelector('meta[name="admin-base"]').content + '/api/media/' + id, { credentials: 'same-origin' }).then(async response => {
|
||||
if (!response.ok || response.redirected) throw new Error('Bild nicht erreichbar');
|
||||
return (await response.json()).item;
|
||||
}).catch(error => { previews.delete(id); throw error; }));
|
||||
return previews.get(id);
|
||||
}
|
||||
|
||||
/** A preview of the current selection, including small originals without a 480 px variant. */
|
||||
export function createMediaPreview(initial = {}) {
|
||||
const wrap = element('figure', '', 'selected-media-preview');
|
||||
const image = element('img'); image.alt = ''; image.decoding = 'async'; image.referrerPolicy = 'no-referrer'; image.hidden = true;
|
||||
const caption = element('figcaption'); caption.setAttribute('aria-live', 'polite');
|
||||
wrap.append(image, caption); let sequence = 0;
|
||||
const update = async ({ src = '', mediaId = '', label = '' } = {}) => {
|
||||
const request = ++sequence; image.hidden = true; image.removeAttribute('src');
|
||||
caption.textContent = src || mediaId ? 'Bildvorschau wird geladen …' : 'Kein Bild ausgewählt';
|
||||
if (!src && !mediaId) return;
|
||||
try {
|
||||
if (!src && /^[a-f0-9]{32}$/.test(mediaId)) {
|
||||
const item = await previewItem(mediaId);
|
||||
if (request !== sequence) return;
|
||||
src = item.variants[0]?.src || ''; label ||= item.title;
|
||||
}
|
||||
const url = new URL(src, location.origin);
|
||||
if (!(src.startsWith('/') && !src.startsWith('//') && url.origin === location.origin) && url.protocol !== 'https:') throw new Error();
|
||||
if (!src) throw new Error();
|
||||
const description = label || decodeURIComponent(url.pathname.split('/').pop()) || 'Ausgewähltes Bild';
|
||||
image.onload = () => { if (request === sequence) { image.hidden = false; caption.textContent = description; } };
|
||||
image.onerror = () => { if (request === sequence) { image.hidden = true; caption.textContent = 'Bildvorschau nicht verfügbar. Bildadresse prüfen oder ein anderes Bild wählen.'; } };
|
||||
image.src = url.href;
|
||||
} catch {
|
||||
if (request === sequence) caption.textContent = 'Bildvorschau nicht verfügbar. Bildadresse prüfen oder ein anderes Bild wählen.';
|
||||
}
|
||||
};
|
||||
update(initial);
|
||||
return { element: wrap, update };
|
||||
}
|
||||
|
||||
export function openMedia({ multiple = false, onChoose } = {}) {
|
||||
const opener = document.activeElement;
|
||||
const dialog = element('dialog', '', 'media-dialog'); dialog.id = 'media-dialog';
|
||||
@@ -24,6 +64,7 @@ export function openMedia({ multiple = false, onChoose } = {}) {
|
||||
if (response.redirected) throw new Error('Sitzung abgelaufen. Bitte erneut anmelden.');
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(data.error || `Medienanfrage fehlgeschlagen (${response.status}).`);
|
||||
for (const item of data.items || (data.item ? [data.item] : [])) previews.set(item.id, Promise.resolve(item));
|
||||
return data;
|
||||
}
|
||||
function render() {
|
||||
|
||||
@@ -9,7 +9,7 @@ let serial = list.children.length, pendingPage = null, busy = false;
|
||||
const el = (tag, className = '', text = '') => { const node = document.createElement(tag); node.className = className; node.textContent = text; return node; };
|
||||
const rows = () => [...list.children].map(row => {
|
||||
const data = { level: Number(row.dataset.level) };
|
||||
row.querySelectorAll('[data-key]').forEach(input => { data[input.dataset.key] = input.value; });
|
||||
row.querySelectorAll('[data-key]').forEach(input => { data[input.dataset.key] = input.type === 'checkbox' ? (input.checked ? '1' : '0') : input.value; });
|
||||
data.level = Number(data.level); data.pageId = row.querySelector('.picker-value')?.value ?? '';
|
||||
data.pageDisplay = row.querySelector('[role="combobox"]')?.value ?? '';
|
||||
return data;
|
||||
@@ -49,7 +49,10 @@ function createRow(data) {
|
||||
const field = (key, value, label, type = 'text') => {
|
||||
const input = el('input'); input.type = type; input.dataset.key = key; input.value = value ?? '';
|
||||
input.id = `menu-${key}-${id}`;
|
||||
if (type !== 'hidden') { const caption = el('label', '', label); caption.htmlFor = input.id; body.append(caption, input); }
|
||||
if (type !== 'hidden') {
|
||||
const caption = el('label', type === 'checkbox' ? 'check-label' : '', label); caption.htmlFor = input.id;
|
||||
if (type === 'checkbox') { caption.prepend(input); body.append(caption); } else body.append(caption, input);
|
||||
}
|
||||
else row.append(input);
|
||||
return input;
|
||||
};
|
||||
@@ -68,6 +71,7 @@ function createRow(data) {
|
||||
body.append(picker); setupPicker(picker);
|
||||
field('anchor', data.anchor, 'Sprungziel (optional)').maxLength = 65;
|
||||
} else { const url = field('url', data.url, 'Adresse'); url.required = true; url.maxLength = 2048; }
|
||||
const newTab = field('newTab', '1', 'In neuem Tab öffnen', 'checkbox'); newTab.checked = data.newTab === true || data.newTab === '1';
|
||||
const actions = el('div', 'menu-entry-actions'); actions.setAttribute('role', 'group');
|
||||
for (const [action, caption] of Object.entries({ up: '↑', down: '↓', indent: 'Als Unterpunkt', outdent: 'Hauptebene', remove: 'Entfernen' })) {
|
||||
const button = el('button', `text-button${action === 'remove' ? ' danger-text' : ''}`, caption); button.type = 'button'; button.dataset.menuAction = action; actions.append(button);
|
||||
@@ -101,7 +105,8 @@ form.querySelector('#menu-add-page').addEventListener('click', () => {
|
||||
form.querySelector('#menu-add-url').addEventListener('click', () => {
|
||||
const name = form.querySelector('#link-label'), url = form.querySelector('#link-url');
|
||||
if (!name.value.trim() || !url.value.trim()) { error.hidden = false; error.textContent = 'Bitte Beschriftung und Adresse des Links ausfüllen.'; (!name.value.trim() ? name : url).focus(); return; }
|
||||
error.hidden = true; add({ type: 'url', label: name.value.trim(), url: url.value.trim(), level: 1 }); name.value = url.value = '';
|
||||
const newTab = form.querySelector('#link-new-tab');
|
||||
error.hidden = true; add({ type: 'url', label: name.value.trim(), url: url.value.trim(), newTab: newTab.checked, level: 1 }); name.value = url.value = ''; newTab.checked = false;
|
||||
});
|
||||
form.addEventListener('input', event => { if (event.target.closest('.menu-structure')) refresh(); });
|
||||
form.addEventListener('change', event => { if (event.target.closest('.menu-structure')) commit(last); });
|
||||
|
||||
@@ -83,7 +83,10 @@ export function createRichText(value, apply, { label = 'Text' } = {}) {
|
||||
linkInput.type = 'text';
|
||||
linkInput.placeholder = '/kontakt, #abschnitt, https://…, mailto:…';
|
||||
linkInput.setAttribute('aria-label', 'Linkziel');
|
||||
let savedRange = null;
|
||||
const newTabLabel = document.createElement('label'); newTabLabel.className = 'check-label';
|
||||
const newTab = document.createElement('input'); newTab.type = 'checkbox';
|
||||
newTabLabel.append(newTab, document.createTextNode(' In neuem Tab öffnen'));
|
||||
let savedRange = null, currentLink = null;
|
||||
const confirmLink = () => {
|
||||
const target = linkInput.value.trim();
|
||||
linkRow.hidden = true;
|
||||
@@ -93,8 +96,16 @@ export function createRichText(value, apply, { label = 'Text' } = {}) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(savedRange);
|
||||
}
|
||||
if (target) document.execCommand('createLink', false, target);
|
||||
else document.execCommand('unlink');
|
||||
if (target) {
|
||||
if (currentLink && surface.contains(currentLink)) currentLink.setAttribute('href', target);
|
||||
else document.execCommand('createLink', false, target);
|
||||
const range = window.getSelection().rangeCount ? window.getSelection().getRangeAt(0) : null;
|
||||
for (const anchor of surface.querySelectorAll('a')) {
|
||||
if (anchor !== currentLink && !(anchor.getAttribute('href') === target && range?.intersectsNode(anchor))) continue;
|
||||
if (newTab.checked) { anchor.setAttribute('target', '_blank'); anchor.setAttribute('rel', 'noopener'); }
|
||||
else { anchor.removeAttribute('target'); anchor.removeAttribute('rel'); }
|
||||
}
|
||||
} else document.execCommand('unlink');
|
||||
push(surface.innerHTML);
|
||||
linkInput.value = '';
|
||||
};
|
||||
@@ -102,14 +113,20 @@ export function createRichText(value, apply, { label = 'Text' } = {}) {
|
||||
if ('Enter' === event.key) { event.preventDefault(); confirmLink(); }
|
||||
if ('Escape' === event.key) { event.preventDefault(); linkRow.hidden = true; surface.focus(); }
|
||||
});
|
||||
linkRow.append(linkInput, button('Übernehmen', 'Linkziel übernehmen', confirmLink, 'text-button'));
|
||||
linkRow.append(linkInput, newTabLabel, button('Übernehmen', 'Linkziel übernehmen', confirmLink, 'text-button'));
|
||||
|
||||
bar.append(button('🔗', 'Link setzen oder entfernen', () => {
|
||||
const selection = window.getSelection();
|
||||
savedRange = selection.rangeCount > 0 ? selection.getRangeAt(0).cloneRange() : null;
|
||||
savedRange = selection.rangeCount > 0 && surface.contains(selection.getRangeAt(0).commonAncestorContainer) ? selection.getRangeAt(0).cloneRange() : null;
|
||||
const node = savedRange?.startContainer;
|
||||
currentLink = (node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement)?.closest('a');
|
||||
if (currentLink && (!surface.contains(currentLink) || !currentLink.contains(savedRange.endContainer))) currentLink = null;
|
||||
linkInput.value = currentLink?.getAttribute('href') || '';
|
||||
newTab.checked = currentLink?.getAttribute('target') === '_blank';
|
||||
linkRow.hidden = false;
|
||||
linkInput.focus();
|
||||
}));
|
||||
linkRow.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); linkRow.hidden = true; surface.focus(); } });
|
||||
|
||||
const toggle = button('HTML', 'Zwischen Formatierung und HTML wechseln', () => {
|
||||
const toSource = source.hidden;
|
||||
|
||||
Reference in new issue
Block a user