Upload PDFs to the library, link them from any link target, show them in a viewer

PDFs go through the same upload field as images, are checked by their content and kept
unchanged in their own table. Every link field can pick one, and a page that links a PDF
opens it in a dialog with the browser's own viewer; touch devices and browsers without a
viewer get a new tab instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-22 07:56:29 +02:00
1 parent 04927b9563
commit e8a9dcdf5c
20 files changed
+667 -30

No files matched your search

+14 -7
View File
@@ -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, createMediaPreview } from './media.js';
import { openMedia, openDocuments, createMediaPreview } from './media.js';
import { createDesignPanel } from './design-panel.js';
import { renderButtonControls, setButtonDesign } from './button-design.js';
import { createRichText } from './rich-text.js';
@@ -330,7 +330,14 @@ function renderContent(block, panel) {
input.addEventListener('blur', () => { history.mergeKey = null; });
if (options.checkbox) container.append(labelEl); else container.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); container.append(help); }
if (key === 'href') field('In neuem Tab öffnen', 'newTab', block.props.newTab === true, { checkbox: true, container });
if (key === 'href') {
// Ein PDF ist ein Linkziel wie jedes andere: seine Adresse steht danach im Feld. Ohne neuen
// Tab zeigt die Website es im Betrachter über der Seite.
const pdf = document.createElement('button'); pdf.type = 'button'; pdf.className = 'secondary href-document'; pdf.textContent = 'PDF aus Mediathek wählen';
pdf.addEventListener('click', () => openDocuments({ onChoose: item => { input.value = item.src; applyValue(); history.mergeKey = null; } }));
container.append(pdf);
field('In neuem Tab öffnen', 'newTab', block.props.newTab === true, { checkbox: true, container });
}
};
if (!block) {
field('Seitentitel', 'title', history.document.title, { maxLength: 160 });
@@ -373,7 +380,7 @@ function renderContent(block, panel) {
field('Symbol', 'icon', block.props.icon, { choices: Object.fromEntries(ICONS.map(name => [name, name])) });
field('Titel', 'title', block.props.title, { maxLength: 120 });
field('Text', 'text', block.props.text, { multiline: true, maxLength: 600 });
field('Linkziel', 'href', block.props.href || '', { help: 'Optional. Lokaler Pfad, Sprungmarke oder HTTPS-Adresse.' });
field('Linkziel', 'href', block.props.href || '', { help: 'Optional. Lokaler Pfad, Sprungmarke oder HTTPS-Adresse – oder ein PDF.' });
}
if (block.type === 'stat') {
field('Zahl', 'value', block.props.value, { maxLength: 16, help: 'Zum Beispiel 15+, 24/7 oder 100 %.' });
@@ -389,7 +396,7 @@ function renderContent(block, panel) {
field('Titel', 'title', block.props.title, { maxLength: 160 });
field('Text', 'text', block.props.text, { multiline: true, maxLength: 600 });
field('Beschriftung der Schaltfläche', 'buttonText', block.props.buttonText, { maxLength: 80 });
field('Linkziel', 'href', block.props.href, { help: 'Lokaler Pfad (/kontakt), Sprungmarke (#abschnitt) oder HTTPS-Adresse.' });
field('Linkziel', 'href', block.props.href, { help: 'Lokaler Pfad (/kontakt), Sprungmarke (#abschnitt) oder HTTPS-Adresse – oder ein PDF.' });
}
if (block.type === 'contact') {
field('Telefon', 'phone', block.props.phone || '', { maxLength: 80, help: 'Wird als anrufbarer Link ausgegeben.' });
@@ -410,7 +417,7 @@ function renderContent(block, panel) {
field('Größe', 'size', String(block.props.size), { number: true, choices: Object.fromEntries(ICON_SIZES.map(size => [String(size), `${size} px`])), help: 'Feste Stufen, weil die Größe eine Klasse ist – ein freier Wert hätte keine und das Symbol wäre unsichtbar.' });
field('Nur Dekoration', 'decorative', block.props.decorative, { checkbox: true, help: 'Dekorative Symbole werden Screenreadern verschwiegen. Trägt das Symbol eine Aussage, nimm das Häkchen weg und beschrifte es.' });
if (!block.props.decorative) field('Beschriftung', 'label', block.props.label, { maxLength: 120 });
field('Linkziel', 'href', block.props.href || '', { help: 'Optional. Lokaler Pfad, Sprungmarke oder HTTPS-Adresse.' });
field('Linkziel', 'href', block.props.href || '', { help: 'Optional. Lokaler Pfad, Sprungmarke oder HTTPS-Adresse – oder ein PDF.' });
}
if (block.type === 'social') {
field('Größe', 'size', String(block.props.size), { number: true, choices: Object.fromEntries(ICON_SIZES.map(size => [String(size), `${size} px`])), help: 'Die Adressen stehen unter Einstellungen → Unternehmensdaten → Soziale Profile. Farbe, Abstand und Ausrichtung unter Design.' });
@@ -527,7 +534,7 @@ function renderContent(block, panel) {
field('Auszug zeigen', 'showExcerpt', block.props.showExcerpt !== false, { checkbox: true });
field('Kategorie zeigen', 'showTerms', block.props.showTerms !== false, { checkbox: true, help: 'Alle drei aus: ein Raster aus Bild und Titel, wie es zu Projekten passt.' });
}
if (block.type === 'button') field('Linkziel', 'href', block.props.href, { help: 'Lokaler Pfad (/kontakt), Sprungmarke (#abschnitt) oder HTTPS-Adresse.' });
if (block.type === 'button') field('Linkziel', 'href', block.props.href, { help: 'Lokaler Pfad (/kontakt), Sprungmarke (#abschnitt) oder HTTPS-Adresse – oder ein PDF.' });
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 });
@@ -536,7 +543,7 @@ function renderContent(block, panel) {
const showLinkOptions = value => { linkOptions.hidden = value !== 'link'; };
const clickAction = block.props?.click || (block.props?.href ? 'link' : 'keine');
field('Beim Klick', 'click', clickAction, { choices: { keine: 'Nichts tun', vergroessern: 'Bild vergrößern', link: 'Zu einer Adresse führen' }, onInput: showLinkOptions });
field('Linkziel', 'href', block.props.href || '', { container: linkOptions, help: 'Lokaler Pfad (/kontakt), Sprungmarke (#abschnitt) oder HTTPS-Adresse. Für ein Logo im Kopfbereich meist /.' });
field('Linkziel', 'href', block.props.href || '', { container: linkOptions, help: 'Lokaler Pfad (/kontakt), Sprungmarke (#abschnitt) oder HTTPS-Adresse – oder ein PDF. Für ein Logo im Kopfbereich meist /.' });
showLinkOptions(clickAction); panel.append(linkOptions);
const picker = document.createElement('button'); picker.className = 'secondary'; picker.textContent = 'Bild aus Mediathek wählen';
picker.addEventListener('click', () => chooseMedia(false)); panel.append(picker);