1053 lines
39 KiB
HTML
1053 lines
39 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>OER Bildlizenzgenerator – TULLU+B</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..700;1,9..40,300..700&family=JetBrains+Mono:wght@400;500&family=Fraunces:ital,opsz,wght@0,9..144,700;1,9..144,400&display=swap" rel="stylesheet">
|
||
<style>
|
||
/* ═══════════════════════════════════════════
|
||
Design Tokens
|
||
═══════════════════════════════════════════ */
|
||
:root {
|
||
--c-bg: #F7F5F0;
|
||
--c-surface: #FFFFFF;
|
||
--c-surface-alt: #F0EDE6;
|
||
--c-border: #DDD8CE;
|
||
--c-border-focus: #1D4ED8;
|
||
--c-text: #1A1A1A;
|
||
--c-text-muted: #6B6560;
|
||
--c-accent: #1D4ED8;
|
||
--c-accent-soft: #DBEAFE;
|
||
--c-accent-dark: #1E3A8A;
|
||
--c-success: #16A34A;
|
||
--c-success-bg: #DCFCE7;
|
||
--c-warn: #D97706;
|
||
--c-warn-bg: #FEF3C7;
|
||
--c-danger: #DC2626;
|
||
--c-danger-bg: #FEE2E2;
|
||
--c-highlight: #1E3A8A;
|
||
--font-body: 'DM Sans', system-ui, sans-serif;
|
||
--font-display: 'Fraunces', Georgia, serif;
|
||
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
||
--radius-sm: 6px;
|
||
--radius-md: 10px;
|
||
--radius-lg: 16px;
|
||
--shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
||
--shadow-md: 0 4px 12px rgba(0,0,0,.07), 0 1px 3px rgba(0,0,0,.04);
|
||
--shadow-lg: 0 10px 30px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.04);
|
||
--transition: 200ms cubic-bezier(.4,0,.2,1);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Reset & Base
|
||
═══════════════════════════════════════════ */
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: var(--font-body);
|
||
font-size: 15px;
|
||
line-height: 1.6;
|
||
color: var(--c-text);
|
||
background: var(--c-bg);
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Layout
|
||
═══════════════════════════════════════════ */
|
||
.app-container {
|
||
max-width: 860px;
|
||
margin: 0 auto;
|
||
padding: 2rem 1.25rem 4rem;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Header
|
||
═══════════════════════════════════════════ */
|
||
.app-header {
|
||
text-align: center;
|
||
margin-bottom: 2.5rem;
|
||
position: relative;
|
||
}
|
||
.app-header::after {
|
||
content: '';
|
||
display: block;
|
||
width: 60px;
|
||
height: 3px;
|
||
background: var(--c-accent);
|
||
border-radius: 2px;
|
||
margin: 1.25rem auto 0;
|
||
}
|
||
.app-header h1 {
|
||
font-family: var(--font-display);
|
||
font-weight: 700;
|
||
font-size: clamp(1.6rem, 4vw, 2.2rem);
|
||
color: var(--c-text);
|
||
letter-spacing: -.02em;
|
||
line-height: 1.2;
|
||
}
|
||
.app-header h1 span {
|
||
background: linear-gradient(135deg, var(--c-accent), #6366F1);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
.app-header .subtitle {
|
||
margin-top: .5rem;
|
||
color: var(--c-text-muted);
|
||
font-size: .9rem;
|
||
}
|
||
.app-header .subtitle a {
|
||
color: var(--c-accent);
|
||
text-decoration: none;
|
||
border-bottom: 1px solid transparent;
|
||
transition: border-color var(--transition);
|
||
}
|
||
.app-header .subtitle a:hover { border-bottom-color: var(--c-accent); }
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Card
|
||
═══════════════════════════════════════════ */
|
||
.card {
|
||
background: var(--c-surface);
|
||
border: 1px solid var(--c-border);
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow-sm);
|
||
margin-bottom: 1.5rem;
|
||
transition: box-shadow var(--transition);
|
||
}
|
||
.card:hover { box-shadow: var(--shadow-md); }
|
||
.card-body { padding: 1.5rem; }
|
||
.card-title {
|
||
font-family: var(--font-display);
|
||
font-weight: 700;
|
||
font-size: 1.15rem;
|
||
color: var(--c-text);
|
||
margin-bottom: 1.25rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: .5rem;
|
||
}
|
||
.card-title .icon {
|
||
width: 28px; height: 28px;
|
||
background: var(--c-accent-soft);
|
||
border-radius: var(--radius-sm);
|
||
display: grid; place-items: center;
|
||
font-size: .85rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Form Elements
|
||
═══════════════════════════════════════════ */
|
||
.form-label {
|
||
display: block;
|
||
font-size: .8rem;
|
||
font-weight: 600;
|
||
color: var(--c-text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: .04em;
|
||
margin-bottom: .35rem;
|
||
}
|
||
.highlight-letter {
|
||
color: var(--c-highlight);
|
||
font-weight: 700;
|
||
font-size: .95em;
|
||
}
|
||
.form-control, .form-select {
|
||
width: 100%;
|
||
padding: .55rem .75rem;
|
||
font-family: var(--font-body);
|
||
font-size: .9rem;
|
||
color: var(--c-text);
|
||
background: var(--c-surface);
|
||
border: 1.5px solid var(--c-border);
|
||
border-radius: var(--radius-md);
|
||
outline: none;
|
||
transition: border-color var(--transition), box-shadow var(--transition);
|
||
}
|
||
.form-control:focus, .form-select:focus {
|
||
border-color: var(--c-border-focus);
|
||
box-shadow: 0 0 0 3px var(--c-accent-soft);
|
||
}
|
||
.form-control::placeholder { color: #B0AAA0; }
|
||
.form-control:disabled, .form-select:disabled {
|
||
background: var(--c-surface-alt);
|
||
opacity: .6;
|
||
cursor: not-allowed;
|
||
}
|
||
textarea.form-control { resize: vertical; min-height: 70px; }
|
||
|
||
.form-select {
|
||
appearance: none;
|
||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%236B6560' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||
background-repeat: no-repeat;
|
||
background-position: right .6rem center;
|
||
background-size: 14px;
|
||
padding-right: 2rem;
|
||
}
|
||
|
||
.form-hint {
|
||
font-size: .78rem;
|
||
color: var(--c-text-muted);
|
||
margin-top: .3rem;
|
||
}
|
||
.form-check {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: .5rem;
|
||
}
|
||
.form-check input[type="checkbox"] {
|
||
width: 18px; height: 18px;
|
||
accent-color: var(--c-accent);
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
}
|
||
.form-check label { font-size: .9rem; cursor: pointer; }
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Grid Helpers
|
||
═══════════════════════════════════════════ */
|
||
.row { display: grid; gap: .75rem; }
|
||
.row-2 { grid-template-columns: 1fr 1fr; }
|
||
.row-3 { grid-template-columns: 1fr 1fr 1fr; }
|
||
.row-4 { grid-template-columns: 1fr 1fr 1fr 2fr; }
|
||
@media (max-width: 640px) {
|
||
.row-2, .row-3, .row-4 { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
.field-group { display: flex; flex-direction: column; }
|
||
.field-group + .field-group { margin-top: .75rem; }
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Author Rows
|
||
═══════════════════════════════════════════ */
|
||
.author-row {
|
||
display: grid;
|
||
grid-template-columns: 1.4fr 1fr auto;
|
||
gap: .5rem;
|
||
align-items: start;
|
||
animation: fadeSlideIn .25s ease;
|
||
}
|
||
@media (max-width: 640px) {
|
||
.author-row { grid-template-columns: 1fr auto; }
|
||
.author-row > :nth-child(2) { grid-column: 1; }
|
||
.author-row > :nth-child(3) { grid-row: 1; grid-column: 2; }
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Buttons
|
||
═══════════════════════════════════════════ */
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: .4rem;
|
||
padding: .55rem 1.1rem;
|
||
font-family: var(--font-body);
|
||
font-size: .85rem;
|
||
font-weight: 600;
|
||
border: 1.5px solid transparent;
|
||
border-radius: var(--radius-md);
|
||
cursor: pointer;
|
||
transition: all var(--transition);
|
||
text-decoration: none;
|
||
line-height: 1.3;
|
||
}
|
||
.btn:active { transform: scale(.97); }
|
||
.btn-primary {
|
||
background: var(--c-accent);
|
||
color: #fff;
|
||
border-color: var(--c-accent);
|
||
}
|
||
.btn-primary:hover { background: var(--c-accent-dark); border-color: var(--c-accent-dark); }
|
||
.btn-secondary {
|
||
background: transparent;
|
||
color: var(--c-text-muted);
|
||
border-color: var(--c-border);
|
||
}
|
||
.btn-secondary:hover { background: var(--c-surface-alt); color: var(--c-text); }
|
||
.btn-icon {
|
||
width: 34px; height: 34px;
|
||
padding: 0; display: grid; place-items: center;
|
||
background: transparent;
|
||
color: var(--c-text-muted);
|
||
border-color: var(--c-border);
|
||
font-size: 1rem;
|
||
}
|
||
.btn-icon:hover { background: var(--c-danger-bg); color: var(--c-danger); border-color: var(--c-danger); }
|
||
.btn-sm { padding: .35rem .75rem; font-size: .8rem; }
|
||
.btn-group { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.25rem; }
|
||
|
||
/* ═══════════════════════════════════════════
|
||
TULLU Badges
|
||
═══════════════════════════════════════════ */
|
||
.tullu-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: .4rem;
|
||
}
|
||
.tullu-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: .3rem;
|
||
padding: .3rem .65rem;
|
||
font-size: .78rem;
|
||
font-weight: 600;
|
||
border-radius: 999px;
|
||
transition: all var(--transition);
|
||
}
|
||
.tullu-badge.ok { background: var(--c-success-bg); color: #166534; }
|
||
.tullu-badge.missing { background: var(--c-warn-bg); color: #92400E; }
|
||
.tullu-badge.neutral { background: var(--c-surface-alt); color: var(--c-text-muted); }
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Output Tabs
|
||
═══════════════════════════════════════════ */
|
||
.output-tabs {
|
||
display: flex;
|
||
border-bottom: 2px solid var(--c-border);
|
||
margin-bottom: 1rem;
|
||
gap: 0;
|
||
overflow-x: auto;
|
||
}
|
||
.output-tab {
|
||
padding: .55rem 1rem;
|
||
font-size: .82rem;
|
||
font-weight: 600;
|
||
color: var(--c-text-muted);
|
||
background: transparent;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
margin-bottom: -2px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: all var(--transition);
|
||
font-family: var(--font-body);
|
||
}
|
||
.output-tab:hover { color: var(--c-text); }
|
||
.output-tab.active {
|
||
color: var(--c-accent);
|
||
border-bottom-color: var(--c-accent);
|
||
}
|
||
.output-panel { display: none; animation: fadeIn .2s ease; }
|
||
.output-panel.active { display: block; }
|
||
|
||
.output-area { position: relative; }
|
||
.output-area textarea {
|
||
min-height: 110px;
|
||
font-family: var(--font-mono);
|
||
font-size: .82rem;
|
||
line-height: 1.55;
|
||
background: var(--c-surface-alt);
|
||
border-color: var(--c-border);
|
||
}
|
||
.output-area textarea:focus {
|
||
background: var(--c-surface);
|
||
}
|
||
|
||
/* Copy Button */
|
||
.copy-btn {
|
||
position: absolute;
|
||
top: .5rem; right: .5rem;
|
||
padding: .3rem .6rem;
|
||
font-size: .75rem;
|
||
font-weight: 600;
|
||
font-family: var(--font-body);
|
||
background: var(--c-surface);
|
||
border: 1.5px solid var(--c-border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--c-text-muted);
|
||
cursor: pointer;
|
||
transition: all var(--transition);
|
||
z-index: 2;
|
||
}
|
||
.copy-btn:hover { background: var(--c-accent-soft); color: var(--c-accent); border-color: var(--c-accent); }
|
||
.copy-btn.copied { background: var(--c-success-bg); color: var(--c-success); border-color: var(--c-success); }
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Preview Figure
|
||
═══════════════════════════════════════════ */
|
||
.preview-figure {
|
||
border: 1px solid var(--c-border);
|
||
border-radius: var(--radius-md);
|
||
padding: 1rem;
|
||
background: var(--c-surface-alt);
|
||
margin-top: .75rem;
|
||
}
|
||
.preview-figure img {
|
||
max-width: 100%;
|
||
border-radius: var(--radius-sm);
|
||
margin-bottom: .75rem;
|
||
display: none;
|
||
}
|
||
.preview-figure figcaption {
|
||
font-size: .85rem;
|
||
line-height: 1.6;
|
||
color: var(--c-text);
|
||
}
|
||
.preview-figure figcaption a {
|
||
color: var(--c-accent);
|
||
text-decoration: none;
|
||
}
|
||
.preview-figure figcaption a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
.preview-label {
|
||
font-size: .75rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: .04em;
|
||
color: var(--c-text-muted);
|
||
margin-bottom: .5rem;
|
||
}
|
||
.md-preview {
|
||
font-size: .85rem;
|
||
line-height: 1.65;
|
||
color: var(--c-text);
|
||
}
|
||
.md-preview strong { font-weight: 700; }
|
||
.md-preview a {
|
||
color: var(--c-accent);
|
||
text-decoration: none;
|
||
}
|
||
.md-preview a:hover { text-decoration: underline; }
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Footer
|
||
═══════════════════════════════════════════ */
|
||
.app-footer {
|
||
text-align: center;
|
||
margin-top: 2rem;
|
||
padding-top: 1.5rem;
|
||
border-top: 1px solid var(--c-border);
|
||
font-size: .78rem;
|
||
color: var(--c-text-muted);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Animations
|
||
═══════════════════════════════════════════ */
|
||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||
@keyframes fadeSlideIn { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="app-container">
|
||
|
||
<!-- ─── Header ─── -->
|
||
<header class="app-header">
|
||
<h1>OER Bildlizenzgenerator <span>TULLU+B</span></h1>
|
||
<p class="subtitle">
|
||
OER-Bildnachweise nach der
|
||
<a href="https://www.orca.nrw/oer/oer-nutzen/tulluba-regel/" target="_blank" rel="noopener">TULLUBA-Regel</a>
|
||
— mit Klartext, HTML, Markdown & JSON-LD
|
||
</p>
|
||
</header>
|
||
|
||
<!-- ─── Eingaben ─── -->
|
||
<section class="card" id="inputCard">
|
||
<div class="card-body">
|
||
<h2 class="card-title"><span class="icon">✏️</span> Eingaben</h2>
|
||
|
||
<div class="field-group">
|
||
<label class="form-label" for="title"><span class="highlight-letter">T</span>itel des Werks</label>
|
||
<input id="title" class="form-control" placeholder="z. B. Sonnenuntergang über Berlin">
|
||
</div>
|
||
|
||
<div class="field-group">
|
||
<label class="form-label"><span class="highlight-letter">U</span>rheber:in / Autor:in</label>
|
||
<div id="authors"></div>
|
||
<button id="addAuthorBtn" type="button" class="btn btn-secondary btn-sm" style="margin-top:.5rem">+ Autor:in</button>
|
||
<p class="form-hint">Mehrere Autor:innen möglich. Leere Zeilen werden ignoriert.</p>
|
||
</div>
|
||
|
||
<div class="row row-2">
|
||
<div class="field-group">
|
||
<label class="form-label" for="license"><span class="highlight-letter">L</span>izenz</label>
|
||
<select id="license" class="form-select"></select>
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="form-label" for="licenseVersion">Version</label>
|
||
<select id="licenseVersion" class="form-select">
|
||
<option value="4.0" selected>4.0</option>
|
||
<option value="3.0">3.0</option>
|
||
<option value="2.0">2.0</option>
|
||
<option value="1.0">1.0</option>
|
||
<option value="">(keine / CC0 / PDM)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row row-2" style="margin-top:.75rem">
|
||
<div class="field-group">
|
||
<label class="form-label" for="workUrl"><span class="highlight-letter">L</span>ink zum Werk</label>
|
||
<input id="workUrl" class="form-control" placeholder="https://…">
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="form-label" for="source"><span class="highlight-letter">U</span>rsprungsort / Plattform</label>
|
||
<input id="source" class="form-control" placeholder="Wikimedia Commons, Flickr …">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row row-4" style="margin-top:.75rem">
|
||
<div class="field-group">
|
||
<label class="form-label" for="year">Jahr</label>
|
||
<input id="year" class="form-control" placeholder="2025" inputmode="numeric">
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="form-label" for="lang">Sprache</label>
|
||
<select id="lang" class="form-select">
|
||
<option value="de" selected>Deutsch</option>
|
||
<option value="en">English</option>
|
||
</select>
|
||
</div>
|
||
<div class="field-group" style="grid-column: span 2">
|
||
<label class="form-label" for="extra">Zusätzliche Hinweise</label>
|
||
<input id="extra" class="form-control" placeholder="z. B. Bildzuschnitt durch die Redaktion">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="field-group" style="margin-top:.75rem">
|
||
<div class="form-check">
|
||
<input type="checkbox" id="isAdapted">
|
||
<label for="isAdapted"><span class="highlight-letter">B</span>earbeitung / Anpassung vorhanden</label>
|
||
</div>
|
||
<textarea id="adaptationNote" class="form-control" style="margin-top:.5rem"
|
||
placeholder="z. B. Zuschnitt, Farbkorrektur; Text hinzugefügt" disabled></textarea>
|
||
</div>
|
||
|
||
<div class="btn-group">
|
||
<button id="buildBtn" type="button" class="btn btn-primary">▶ Lizenzhinweis erzeugen</button>
|
||
<button id="exampleBtn" type="button" class="btn btn-secondary">Beispiel laden</button>
|
||
<button id="resetBtn" type="button" class="btn btn-secondary">Zurücksetzen</button>
|
||
</div>
|
||
|
||
<p class="form-hint" style="margin-top:.75rem">
|
||
Pflicht bei CC BY / CC BY-SA: Urheber:in, Lizenzname und -link. Bei Bearbeitungen immer kenntlich machen (+B).
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ─── TULLU-Check ─── -->
|
||
<section class="card" id="checkCard">
|
||
<div class="card-body">
|
||
<h2 class="card-title"><span class="icon">✅</span> TULLU-Check</h2>
|
||
<div class="tullu-grid" id="tulluBadges" aria-live="polite"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ─── Ausgabe ─── -->
|
||
<section class="card" id="outputCard">
|
||
<div class="card-body">
|
||
<h2 class="card-title"><span class="icon">📋</span> Ausgabe</h2>
|
||
|
||
<div class="output-tabs" role="tablist">
|
||
<button class="output-tab active" data-tab="plaintext" role="tab">Klartext</button>
|
||
<button class="output-tab" data-tab="figcaption" role="tab">HTML-Figcaption</button>
|
||
<button class="output-tab" data-tab="markdown" role="tab">Markdown</button>
|
||
<button class="output-tab" data-tab="jsonld" role="tab">JSON-LD</button>
|
||
</div>
|
||
|
||
<!-- Klartext -->
|
||
<div class="output-panel active" data-panel="plaintext">
|
||
<p style="font-size:.82rem;color:var(--c-text-muted);margin-bottom:.5rem">Für Impressum, Bildnachweis-Listen oder Alt-Text.</p>
|
||
<div class="output-area">
|
||
<button class="copy-btn" data-target="outText">Kopieren</button>
|
||
<textarea id="outText" class="form-control" readonly></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Figcaption -->
|
||
<div class="output-panel" data-panel="figcaption">
|
||
<p style="font-size:.82rem;color:var(--c-text-muted);margin-bottom:.5rem">HTML-Snippet zum Einfügen unter Bildern.</p>
|
||
<div class="output-area">
|
||
<button class="copy-btn" data-target="outFigcaption">Kopieren</button>
|
||
<textarea id="outFigcaption" class="form-control" readonly></textarea>
|
||
</div>
|
||
<figure class="preview-figure">
|
||
<p class="preview-label">Vorschau (gerendert):</p>
|
||
<img id="previewImage" src="#" alt="Vorschau-Bild">
|
||
<figcaption id="figcaptionPreview"></figcaption>
|
||
</figure>
|
||
</div>
|
||
|
||
<!-- Markdown -->
|
||
<div class="output-panel" data-panel="markdown">
|
||
<p style="font-size:.82rem;color:var(--c-text-muted);margin-bottom:.5rem">Für README-Dateien, Wiki-Seiten, Markdown-Dokumente.</p>
|
||
<div class="output-area">
|
||
<button class="copy-btn" data-target="outMarkdown">Kopieren</button>
|
||
<textarea id="outMarkdown" class="form-control" readonly></textarea>
|
||
</div>
|
||
<div class="preview-figure" id="markdownPreview">
|
||
<p class="preview-label">Vorschau (gerendert):</p>
|
||
<div id="markdownPreviewContent" class="md-preview"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- JSON-LD -->
|
||
<div class="output-panel" data-panel="jsonld">
|
||
<p style="font-size:.82rem;color:var(--c-text-muted);margin-bottom:.5rem">Strukturierte Daten für SEO & maschinelle Auswertung.</p>
|
||
<div class="output-area">
|
||
<button class="copy-btn" data-target="outJsonLd">Kopieren</button>
|
||
<textarea id="outJsonLd" class="form-control" readonly style="min-height:180px"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<footer class="app-footer">
|
||
Generiert nach der <a href="https://www.orca.nrw/oer/oer-nutzen/tulluba-regel/" target="_blank" rel="noopener">TULLUBA-Regel</a> ·
|
||
Keine Gewähr für rechtliche Vollständigkeit
|
||
</footer>
|
||
</div>
|
||
|
||
<script>
|
||
/* ═══════════════════════════════════════════════════════
|
||
TULLU+B Bildlizenzgenerator – Refactored
|
||
═══════════════════════════════════════════════════════ */
|
||
|
||
// ─── License Database ───────────────────────────────
|
||
const LICENSES = [
|
||
{ id: 'CC0', label: 'CC0 (Public Domain Waiver)', hasVersion: false, urlFn: () => 'https://creativecommons.org/publicdomain/zero/1.0/' },
|
||
{ id: 'PDM', label: 'Public Domain Mark (PDM)', hasVersion: false, urlFn: () => 'https://creativecommons.org/publicdomain/mark/1.0/' },
|
||
{ id: 'BY', label: 'CC BY', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by/${v || '4.0'}/` },
|
||
{ id: 'BY-SA', label: 'CC BY-SA', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by-sa/${v || '4.0'}/` },
|
||
{ id: 'BY-NC', label: 'CC BY-NC', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by-nc/${v || '4.0'}/` },
|
||
{ id: 'BY-NC-SA', label: 'CC BY-NC-SA', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by-nc-sa/${v || '4.0'}/` },
|
||
{ id: 'BY-ND', label: 'CC BY-ND', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by-nd/${v || '4.0'}/` },
|
||
{ id: 'BY-NC-ND', label: 'CC BY-NC-ND', hasVersion: true, urlFn: (v) => `https://creativecommons.org/licenses/by-nc-nd/${v || '4.0'}/` },
|
||
{ id: 'CUSTOM', label: 'Benutzerdefiniert', hasVersion: true, urlFn: () => null },
|
||
];
|
||
|
||
const LICENSE_ALIASES = Object.fromEntries([
|
||
['cc0','CC0'], ['pdm','PDM'],
|
||
['by','BY'], ['cc-by','BY'],
|
||
['by-sa','BY-SA'], ['cc-by-sa','BY-SA'], ['by_sa','BY-SA'],
|
||
['by-nc','BY-NC'], ['cc-by-nc','BY-NC'], ['by_nc','BY-NC'],
|
||
['by-nc-sa','BY-NC-SA'], ['cc-by-nc-sa','BY-NC-SA'], ['by_nc_sa','BY-NC-SA'],
|
||
['by-nd','BY-ND'], ['cc-by-nd','BY-ND'], ['by_nd','BY-ND'],
|
||
['by-nc-nd','BY-NC-ND'], ['cc-by-nc-nd','BY-NC-ND'], ['by_nc_nd','BY-NC-ND'],
|
||
['custom','CUSTOM'],
|
||
].map(([k, v]) => [k, v]));
|
||
|
||
// ─── Utilities ──────────────────────────────────────
|
||
const $ = (sel) => document.querySelector(sel);
|
||
const $$ = (sel) => document.querySelectorAll(sel);
|
||
|
||
function escapeHTML(str) {
|
||
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
||
return String(str).replace(/[&<>"']/g, (ch) => map[ch]);
|
||
}
|
||
|
||
function escapeAttr(str) {
|
||
return String(str).replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
|
||
function getLicenseDef(id) {
|
||
return LICENSES.find((l) => l.id === id) || LICENSES[0];
|
||
}
|
||
|
||
function getLicenseLabel(licDef, version) {
|
||
if (licDef.id === 'CC0') return 'CC0 1.0';
|
||
if (licDef.id === 'PDM') return 'Public Domain Mark 1.0';
|
||
if (licDef.id === 'CUSTOM') return 'Benutzerdefinierte Lizenz';
|
||
return `${licDef.label} ${version || ''}`.trim();
|
||
}
|
||
|
||
// ─── Author Management ──────────────────────────────
|
||
const authorsContainer = $('#authors');
|
||
|
||
function addAuthor(name = '', url = '') {
|
||
const row = document.createElement('div');
|
||
row.className = 'author-row';
|
||
row.innerHTML = `
|
||
<input class="form-control" placeholder="Name" value="${escapeAttr(name)}">
|
||
<input class="form-control" placeholder="URL (optional)" value="${escapeAttr(url)}">
|
||
<button type="button" class="btn btn-icon" title="Entfernen">✕</button>
|
||
`;
|
||
row.querySelector('button').addEventListener('click', () => {
|
||
row.remove();
|
||
ensureOneAuthor();
|
||
});
|
||
authorsContainer.appendChild(row);
|
||
}
|
||
|
||
function getAuthors() {
|
||
return Array.from(authorsContainer.querySelectorAll('.author-row'))
|
||
.map((row) => {
|
||
const inputs = row.querySelectorAll('input');
|
||
const name = inputs[0].value.trim();
|
||
const url = inputs[1].value.trim();
|
||
return name ? { name, url: url || null } : null;
|
||
})
|
||
.filter(Boolean);
|
||
}
|
||
|
||
function clearAuthors() { authorsContainer.innerHTML = ''; }
|
||
function ensureOneAuthor() { if (!authorsContainer.querySelector('.author-row')) addAuthor(); }
|
||
|
||
// ─── Populate License Select ────────────────────────
|
||
const licenseSelect = $('#license');
|
||
LICENSES.forEach((lic) => {
|
||
const opt = document.createElement('option');
|
||
opt.value = lic.id;
|
||
opt.textContent = lic.label;
|
||
licenseSelect.appendChild(opt);
|
||
});
|
||
licenseSelect.value = 'BY';
|
||
|
||
// ─── Read Form Data ─────────────────────────────────
|
||
function readForm() {
|
||
return {
|
||
title: $('#title').value.trim(),
|
||
creators: getAuthors(),
|
||
licenseId: $('#license').value,
|
||
licenseVersion: $('#licenseVersion').value,
|
||
workUrl: $('#workUrl').value.trim(),
|
||
source: $('#source').value.trim(),
|
||
year: $('#year').value.trim(),
|
||
isAdapted: $('#isAdapted').checked,
|
||
adaptationNote: $('#adaptationNote').value.trim(),
|
||
extra: $('#extra').value.trim(),
|
||
lang: $('#lang').value,
|
||
};
|
||
}
|
||
|
||
// ─── Attribution Formatting ─────────────────────────
|
||
function formatAttribution(data) {
|
||
const licDef = getLicenseDef(data.licenseId);
|
||
const licUrl = licDef.urlFn(data.licenseVersion) || '';
|
||
const licLabel = getLicenseLabel(licDef, data.licenseVersion);
|
||
const isDE = data.lang === 'de';
|
||
|
||
const title = data.title || (isDE ? '(ohne Titel)' : '(untitled)');
|
||
const unknownCreator = isDE ? '(Urheber:in unbekannt)' : '(creator unknown)';
|
||
|
||
// ── Creator Strings ──
|
||
const creatorsPlain = data.creators.length
|
||
? data.creators.map((c) => c.url ? `${c.name} (${c.url})` : c.name).join(', ')
|
||
: unknownCreator;
|
||
|
||
const creatorsHTML = data.creators.length
|
||
? data.creators.map((c) => c.url
|
||
? `<a href="${escapeAttr(c.url)}" target="_blank" rel="noopener">${escapeHTML(c.name)}</a>`
|
||
: escapeHTML(c.name)
|
||
).join(', ')
|
||
: unknownCreator;
|
||
|
||
const creatorsMD = data.creators.length
|
||
? data.creators.map((c) => c.url ? `[${c.name}](${c.url})` : c.name).join(', ')
|
||
: unknownCreator;
|
||
|
||
// Labels
|
||
const lbl = {
|
||
by: isDE ? 'von' : 'by',
|
||
lic: isDE ? 'lizenziert unter' : 'licensed under',
|
||
licC: isDE ? 'Lizenz' : 'License',
|
||
src: isDE ? 'Quelle' : 'Source',
|
||
link: 'Link',
|
||
changes: isDE ? 'Änderungen' : 'Changes',
|
||
title: 'Titel',
|
||
author: isDE ? 'Urheber:in' : 'Creator',
|
||
origin: isDE ? 'Ursprungsort' : 'Source',
|
||
};
|
||
|
||
const adaptNote = data.adaptationNote || (isDE ? 'Bearbeitung vorhanden' : 'Adaptation present');
|
||
|
||
// ── 1) Klartext ──
|
||
const textParts = [
|
||
`„${title}"`,
|
||
`${lbl.by} ${creatorsPlain}`,
|
||
];
|
||
if (data.year) textParts.push(data.year);
|
||
textParts.push(
|
||
licDef.id === 'CUSTOM'
|
||
? `${lbl.licC}: ${licLabel}`
|
||
: `${lbl.lic} ${licLabel}`
|
||
);
|
||
if (data.source) textParts.push(`${lbl.src}: ${data.source}`);
|
||
if (data.workUrl) textParts.push(`${lbl.link}: ${data.workUrl}`);
|
||
if (data.isAdapted) textParts.push(`${lbl.changes}: ${adaptNote}`);
|
||
if (data.extra) textParts.push(data.extra);
|
||
const plainText = textParts.join(' · ');
|
||
|
||
// ── 2) HTML Figcaption ──
|
||
const licHtml = licDef.id === 'CUSTOM'
|
||
? escapeHTML(licLabel)
|
||
: `<a href="${licUrl}" target="_blank" rel="license noopener">${escapeHTML(licLabel)}</a>`;
|
||
|
||
const figLines = [
|
||
`${lbl.title}: „${escapeHTML(data.title || '')}"`,
|
||
`${lbl.author}: ${creatorsHTML}`,
|
||
`${lbl.licC}: ${licHtml}`,
|
||
`${lbl.origin}: ${data.source ? escapeHTML(data.source) : '–'}`,
|
||
`${lbl.link}: ${data.workUrl ? `<a href="${data.workUrl}" target="_blank" rel="noopener">${escapeHTML(data.workUrl)}</a>` : '–'}`,
|
||
];
|
||
if (data.isAdapted) figLines.push(`${lbl.changes}: ${escapeHTML(adaptNote)}`);
|
||
if (data.extra) figLines.push(escapeHTML(data.extra));
|
||
|
||
const figcaptionInner = figLines.join('<br>\n ');
|
||
const figcaptionFull = `<figcaption>\n ${figcaptionInner}\n</figcaption>`;
|
||
|
||
// ── 3) Markdown ──
|
||
const licMD = licDef.id === 'CUSTOM'
|
||
? licLabel
|
||
: licUrl ? `[${licLabel}](${licUrl})` : licLabel;
|
||
|
||
const mdLines = [
|
||
`**${lbl.title}:** „${data.title || ''}" `,
|
||
`**${lbl.author}:** ${creatorsMD} `,
|
||
`**${lbl.licC}:** ${licMD} `,
|
||
`**${lbl.origin}:** ${data.source || '–'} `,
|
||
`**${lbl.link}:** ${data.workUrl ? `<${data.workUrl}>` : '–'} `,
|
||
];
|
||
if (data.isAdapted) mdLines.push(`**${lbl.changes}:** ${adaptNote} `);
|
||
if (data.extra) mdLines.push(data.extra + ' ');
|
||
const markdown = mdLines.join('\n');
|
||
|
||
// ── 4) JSON-LD ──
|
||
const creatorsJson = data.creators.map((c) => ({
|
||
'@type': 'Person',
|
||
name: c.name,
|
||
...(c.url ? { url: c.url } : {}),
|
||
}));
|
||
const jsonObj = {
|
||
'@context': 'https://schema.org',
|
||
'@type': 'ImageObject',
|
||
name: data.title || '',
|
||
...(data.workUrl ? { contentUrl: data.workUrl } : {}),
|
||
...(data.source ? { isBasedOn: data.source } : {}),
|
||
...(data.year ? { dateCreated: data.year } : {}),
|
||
creator: creatorsJson,
|
||
...(licUrl ? { license: licUrl } : {}),
|
||
};
|
||
if (data.isAdapted) jsonObj.creditText = `Adaptation: ${adaptNote}`;
|
||
if (data.extra) jsonObj.comment = data.extra;
|
||
const jsonLD = JSON.stringify(jsonObj, null, 2);
|
||
|
||
// ── TULLU Check ──
|
||
const check = {
|
||
T: !!data.title,
|
||
U: data.creators.length > 0,
|
||
L: !!licLabel,
|
||
L2: licDef.id === 'CUSTOM' || !!licUrl,
|
||
U2: !!data.workUrl,
|
||
U3: !!data.source,
|
||
B: data.isAdapted ? !!data.adaptationNote : null,
|
||
};
|
||
|
||
return { plainText, figcaptionFull, figcaptionInner, markdown, jsonLD, check };
|
||
}
|
||
|
||
// ─── TULLU Badges ───────────────────────────────────
|
||
const TULLU_ITEMS = [
|
||
['T', 'Titel'],
|
||
['U', 'Urheber:in'],
|
||
['L', 'Lizenz'],
|
||
['L2', 'Lizenz-Link'],
|
||
['U2', 'Werk-Link'],
|
||
['U3', 'Ursprungsort'],
|
||
['B', '+ Bearbeitung'],
|
||
];
|
||
|
||
function renderBadges(check) {
|
||
const container = $('#tulluBadges');
|
||
container.innerHTML = '';
|
||
TULLU_ITEMS.forEach(([key, label]) => {
|
||
const val = check[key];
|
||
const badge = document.createElement('span');
|
||
if (val === null || val === undefined) {
|
||
badge.className = 'tullu-badge neutral';
|
||
badge.textContent = `${label} –`;
|
||
} else if (val) {
|
||
badge.className = 'tullu-badge ok';
|
||
badge.textContent = `${label} ✓`;
|
||
} else {
|
||
badge.className = 'tullu-badge missing';
|
||
badge.textContent = `${label} ○`;
|
||
}
|
||
container.appendChild(badge);
|
||
});
|
||
}
|
||
|
||
// ─── Simple Markdown → HTML (for preview) ───────────
|
||
function renderMarkdownToHTML(md) {
|
||
return md
|
||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
|
||
.replace(/<(https?:\/\/[^>]+)>/g, '<a href="$1" target="_blank" rel="noopener">$1</a>')
|
||
.replace(/„([^"]+)"/g, '„$1"')
|
||
.replace(/ \n/g, '<br>\n')
|
||
.replace(/ $/gm, '<br>');
|
||
}
|
||
|
||
// ─── Build All Outputs ──────────────────────────────
|
||
function buildAll() {
|
||
const data = readForm();
|
||
const result = formatAttribution(data);
|
||
|
||
$('#outText').value = result.plainText;
|
||
$('#outFigcaption').value = result.figcaptionFull;
|
||
$('#outMarkdown').value = result.markdown;
|
||
$('#outJsonLd').value = result.jsonLD;
|
||
$('#figcaptionPreview').innerHTML = result.figcaptionInner;
|
||
$('#markdownPreviewContent').innerHTML = renderMarkdownToHTML(result.markdown);
|
||
|
||
const img = $('#previewImage');
|
||
if (data.workUrl) {
|
||
img.src = data.workUrl;
|
||
img.style.display = 'block';
|
||
} else {
|
||
img.style.display = 'none';
|
||
}
|
||
|
||
renderBadges(result.check);
|
||
}
|
||
|
||
// ─── Tab Switching ──────────────────────────────────
|
||
$$('.output-tab').forEach((tab) => {
|
||
tab.addEventListener('click', () => {
|
||
$$('.output-tab').forEach((t) => t.classList.remove('active'));
|
||
$$('.output-panel').forEach((p) => p.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
const panel = $(`[data-panel="${tab.dataset.tab}"]`);
|
||
if (panel) panel.classList.add('active');
|
||
});
|
||
});
|
||
|
||
// ─── Copy Buttons ───────────────────────────────────
|
||
$$('.copy-btn').forEach((btn) => {
|
||
btn.addEventListener('click', () => {
|
||
const textarea = $(`#${btn.dataset.target}`);
|
||
if (!textarea) return;
|
||
navigator.clipboard.writeText(textarea.value).then(() => {
|
||
btn.textContent = '✓ Kopiert!';
|
||
btn.classList.add('copied');
|
||
setTimeout(() => {
|
||
btn.textContent = 'Kopieren';
|
||
btn.classList.remove('copied');
|
||
}, 1500);
|
||
});
|
||
});
|
||
});
|
||
|
||
// ─── Event Listeners ────────────────────────────────
|
||
$('#isAdapted').addEventListener('change', (e) => {
|
||
const note = $('#adaptationNote');
|
||
note.disabled = !e.target.checked;
|
||
if (!e.target.checked) note.value = '';
|
||
});
|
||
|
||
$('#license').addEventListener('change', () => {
|
||
const def = getLicenseDef(licenseSelect.value);
|
||
const versionSel = $('#licenseVersion');
|
||
versionSel.disabled = !def.hasVersion;
|
||
if (!def.hasVersion) versionSel.value = '';
|
||
});
|
||
|
||
$('#buildBtn').addEventListener('click', buildAll);
|
||
$('#addAuthorBtn').addEventListener('click', () => addAuthor());
|
||
|
||
$('#exampleBtn').addEventListener('click', () => {
|
||
$('#title').value = 'Byzantion - Dionysos und Vogel Strauß';
|
||
clearAuthors();
|
||
addAuthor('Reinhard Saczewski', '');
|
||
$('#license').value = 'PDM';
|
||
$('#license').dispatchEvent(new Event('change'));
|
||
$('#licenseVersion').value = '';
|
||
$('#workUrl').value = 'https://commons.wikimedia.org/wiki/File:Byzantion_-_M%C3%BCnzkabinett,_Berlin_-_5533682.jpg';
|
||
$('#source').value = 'Wikimedia Commons';
|
||
$('#year').value = '2022';
|
||
$('#isAdapted').checked = false;
|
||
$('#adaptationNote').disabled = true;
|
||
$('#adaptationNote').value = '';
|
||
$('#extra').value = '';
|
||
$('#lang').value = 'de';
|
||
buildAll();
|
||
});
|
||
|
||
$('#resetBtn').addEventListener('click', () => {
|
||
['title', 'workUrl', 'source', 'year', 'extra'].forEach((id) => $(`#${id}`).value = '');
|
||
$('#isAdapted').checked = false;
|
||
$('#adaptationNote').value = '';
|
||
$('#adaptationNote').disabled = true;
|
||
clearAuthors();
|
||
ensureOneAuthor();
|
||
$('#license').value = 'BY';
|
||
$('#licenseVersion').value = '4.0';
|
||
$('#licenseVersion').disabled = false;
|
||
$('#lang').value = 'de';
|
||
['outText', 'outFigcaption', 'outMarkdown', 'outJsonLd'].forEach((id) => $(`#${id}`).value = '');
|
||
$('#figcaptionPreview').innerHTML = '';
|
||
$('#markdownPreviewContent').innerHTML = '';
|
||
$('#previewImage').style.display = 'none';
|
||
renderBadges({});
|
||
});
|
||
|
||
// ─── URL Prefill ────────────────────────────────────
|
||
function applyQueryParams() {
|
||
const params = new URLSearchParams(location.search);
|
||
if (![...params.keys()].length) return;
|
||
|
||
const setVal = (id, val) => {
|
||
if (val) $(`#${id}`).value = val;
|
||
};
|
||
|
||
setVal('title', params.get('title') || params.get('t'));
|
||
|
||
// Authors
|
||
let authorEntries = params.getAll('author');
|
||
if (!authorEntries.length) {
|
||
const agg = params.get('authors');
|
||
if (agg) authorEntries = agg.split(';').map((s) => s.trim()).filter(Boolean);
|
||
}
|
||
if (authorEntries.length) {
|
||
clearAuthors();
|
||
authorEntries.forEach((entry) => {
|
||
const [name, url] = entry.split('|');
|
||
addAuthor((name || '').trim(), (url || '').trim());
|
||
});
|
||
}
|
||
|
||
// License
|
||
let lic = params.get('license') || params.get('lic') || params.get('l');
|
||
if (lic) {
|
||
lic = lic.toLowerCase().replace(/\s+/g, '').replace(/_/g, '-');
|
||
const normalized = LICENSE_ALIASES[lic] || lic.toUpperCase();
|
||
if (LICENSES.some((l) => l.id === normalized)) licenseSelect.value = normalized;
|
||
}
|
||
const ver = params.get('licenseVersion') || params.get('lv') || params.get('v');
|
||
if (ver != null) setVal('licenseVersion', ver);
|
||
|
||
setVal('workUrl', params.get('work') || params.get('workUrl') || params.get('url') || params.get('link'));
|
||
setVal('source', params.get('source') || params.get('sourceUrl') || params.get('src'));
|
||
setVal('year', params.get('year') || params.get('y'));
|
||
|
||
const lang = (params.get('lang') || '').toLowerCase();
|
||
if (lang === 'en' || lang === 'de') setVal('lang', lang);
|
||
setVal('extra', params.get('extra') || params.get('note'));
|
||
|
||
const adapted = params.get('adapted') || params.get('b') || params.get('isAdapted');
|
||
if (adapted && /^(1|true|yes|ja|y)$/i.test(adapted)) {
|
||
$('#isAdapted').checked = true;
|
||
$('#adaptationNote').disabled = false;
|
||
}
|
||
setVal('adaptationNote', params.get('changes') || params.get('adaptation') || params.get('adaptationNote'));
|
||
}
|
||
|
||
// ─── Init ───────────────────────────────────────────
|
||
function init() {
|
||
ensureOneAuthor();
|
||
licenseSelect.dispatchEvent(new Event('change'));
|
||
applyQueryParams();
|
||
buildAll();
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
</script>
|
||
</body>
|
||
</html>
|