mirror of
https://github.com/Theodor-Springmann-Stiftung/kgpz_web.git
synced 2025-10-28 08:35:30 +00:00
count ontinuations
This commit is contained in:
231
GEMINI.md
Normal file
231
GEMINI.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
KGPZ Web is a Go-based web application for serving historical newspaper data from the KGPZ (Königsberger Gelehrte und Politische Zeitung) digital archive. The application combines server-rendered HTML with HTMX for progressive enhancement, providing a modern web interface for browsing historical content.
|
||||
|
||||
## Architecture
|
||||
|
||||
The application follows a modular Go architecture:
|
||||
|
||||
- **Main Application**: `kgpz_web.go` - Entry point and application lifecycle management
|
||||
- **App Core**: `app/kgpz.go` - Core business logic and data processing
|
||||
- **Controllers**: Route handlers for different content types (issues, agents, places, categories, search)
|
||||
- **View Models**: Data structures for template rendering with pre-processed business logic (`viewmodels/`)
|
||||
- **XML Models**: Data structures for parsing source XML files (`xmlmodels/`)
|
||||
- **Providers**: External service integrations (Git, GND, XML parsing, search)
|
||||
- **Templating**: Custom template engine with Go template integration and helper functions
|
||||
- **Views**: Frontend assets and templates in `views/` directory
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **Data Sources**: XML files from Git repository containing historical newspaper metadata
|
||||
2. **Search**: Full-text search powered by Bleve search engine
|
||||
3. **External Integrations**: GND (Gemeinsame Normdatei) for person metadata, Geonames for place data
|
||||
4. **Template System**: Custom engine supporting layouts and partials with embedded filesystem and helper functions
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Go Backend
|
||||
```bash
|
||||
# Run the application in development mode
|
||||
go run kgpz_web.go
|
||||
|
||||
# Build the application
|
||||
go build -o kgpz_web kgpz_web.go
|
||||
|
||||
# Run tests
|
||||
go test ./helpers/xsdtime/
|
||||
|
||||
# Format code
|
||||
go fmt ./...
|
||||
|
||||
# Check for issues
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
### Frontend Assets (from views/ directory)
|
||||
```bash
|
||||
cd views/
|
||||
|
||||
# Development server with hot reloading
|
||||
npm run dev
|
||||
|
||||
# Build production assets
|
||||
npm run build
|
||||
|
||||
# Build CSS with Tailwind
|
||||
npm run tailwind
|
||||
|
||||
# Build CSS with PostCSS
|
||||
npm run css
|
||||
|
||||
# Preview built assets
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The application uses JSON configuration files:
|
||||
- `config.dev.json` - Development configuration (if exists)
|
||||
- `config.json` - Default configuration (fallback)
|
||||
|
||||
Key configuration options:
|
||||
- `git_url`: Source repository URL for data
|
||||
- `git_branch`: Branch to use for data
|
||||
- `webhook_endpoint`: GitHub webhook endpoint for auto-updates
|
||||
- `debug`: Enable debug mode and logging
|
||||
- `watch`: Enable file watching for template hot-reloading
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **Startup**: Application clones/pulls Git repository with XML data
|
||||
2. **Parsing**: XML files parsed into structured models (agents, places, works, issues)
|
||||
3. **Enrichment**: External APIs (GND, Geonames) enrich metadata
|
||||
4. **Indexing**: Full-text search index built using Bleve
|
||||
5. **Serving**: HTTP server serves templated content with HTMX interactions
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- **Web Framework**: Fiber (high-performance HTTP framework)
|
||||
- **Search**: Bleve (full-text search engine)
|
||||
- **Git Operations**: go-git (Git repository operations)
|
||||
- **Frontend**: HTMX + Tailwind CSS for progressive enhancement
|
||||
- **Build Tools**: Vite for asset bundling, PostCSS for CSS processing
|
||||
|
||||
## Template Structure
|
||||
|
||||
Templates are embedded in the binary:
|
||||
- **Layouts**: `views/layouts/` - Base page structures
|
||||
- **Routes**: `views/routes/` - Page-specific templates
|
||||
- **Assets**: `views/assets/` - Compiled CSS and static files
|
||||
|
||||
The template system supports nested layouts and automatic reloading in development mode when `watch: true` is enabled.
|
||||
|
||||
## Views Directory Structure
|
||||
|
||||
The `views/` directory contains all frontend templates, assets, and build configuration:
|
||||
|
||||
### Directory Layout
|
||||
```
|
||||
views/
|
||||
├── layouts/ # Base templates and layouts
|
||||
│ ├── components/ # Shared layout components (_header, _footer, _menu)
|
||||
│ └── default/ # Default layout (root.gohtml)
|
||||
├── routes/ # Page-specific templates
|
||||
│ ├── akteure/ # Agents/People pages (body.gohtml, head.gohtml)
|
||||
│ ├── ausgabe/ # Issue pages with components
|
||||
│ │ └── components/ # Issue-specific components (_inhaltsverzeichnis, _bilder, etc.)
|
||||
│ ├── components/ # Shared route components (_akteur.gohtml)
|
||||
│ ├── datenschutz/ # Privacy policy
|
||||
│ ├── edition/ # Edition pages
|
||||
│ ├── kategorie/ # Category pages
|
||||
│ ├── kontakt/ # Contact pages
|
||||
│ ├── ort/ # Places pages
|
||||
│ ├── search/ # Search pages
|
||||
│ └── zitation/ # Citation pages
|
||||
├── assets/ # Compiled output assets
|
||||
│ ├── css/ # Compiled CSS files
|
||||
│ ├── js/ # JavaScript libraries and compiled scripts
|
||||
│ ├── fonts/ # Font files
|
||||
│ ├── logo/ # Logo and favicon files
|
||||
│ └── xslt/ # XSLT transformation files
|
||||
├── public/ # Static public assets
|
||||
├── transform/ # Source files for build process
|
||||
│ ├── main.js # Main JavaScript entry point
|
||||
│ └── site.css # Source CSS with Tailwind directives
|
||||
└── node_modules/ # NPM dependencies
|
||||
```
|
||||
|
||||
### Template System
|
||||
|
||||
**Layout Templates** (`layouts/`):
|
||||
- `default/root.gohtml`: Base HTML structure with head, HTMX, Alpine.js setup
|
||||
- `components/_header.gohtml`: Site header with navigation
|
||||
- `components/_footer.gohtml`: Site footer
|
||||
- `components/_menu.gohtml`: Main navigation menu
|
||||
|
||||
**Route Templates** (`routes/`):
|
||||
Each route has dedicated `head.gohtml` and `body.gohtml` files following Go template conventions:
|
||||
- Pages use German naming: `akteure` (agents), `ausgabe` (issues), `ort` (places), etc.
|
||||
- Component partials prefixed with `_` (e.g., `_akteur.gohtml`, `_inhaltsverzeichnis.gohtml`)
|
||||
- HTMX-powered interactions for dynamic content loading
|
||||
|
||||
**Template Features**:
|
||||
- Go template syntax with custom functions from `templating/engine.go`
|
||||
- Block template inheritance system
|
||||
- HTMX integration for progressive enhancement
|
||||
- Conditional development/production asset loading
|
||||
- Template helper functions for UI components (PageIcon, BeilagePageIcon)
|
||||
- Pre-processed view models to minimize template logic
|
||||
|
||||
### Frontend Assets
|
||||
|
||||
**JavaScript Stack**:
|
||||
- **HTMX**: Core interactivity and AJAX requests
|
||||
- **Alpine.js**: Lightweight reactivity for UI components
|
||||
- **Custom Extensions**: HTMX plugins for response targets, client-side templates, loading states
|
||||
- **Build Tool**: Vite for module bundling and development server
|
||||
|
||||
**CSS Stack**:
|
||||
- **Tailwind CSS v4**: Utility-first CSS framework
|
||||
- **PostCSS**: CSS processing pipeline
|
||||
- **RemixIcon**: Icon font library
|
||||
- **Custom Fonts**: Typography setup in `assets/css/fonts.css`
|
||||
|
||||
**Build Process**:
|
||||
- **Source**: `transform/main.js` and `transform/site.css`
|
||||
- **Output**: Compiled to `assets/scripts.js` and `assets/style.css`
|
||||
- **Vite Config**: Production build targeting ES modules
|
||||
- **PostCSS Config**: Tailwind CSS processing
|
||||
|
||||
### Asset Loading Strategy
|
||||
|
||||
The root template conditionally loads assets based on environment:
|
||||
- Development: Uses dev favicon, enables hot reloading
|
||||
- Production: Optimized assets, production favicon
|
||||
- Module imports: ES6 modules with `setup()` function from compiled scripts
|
||||
- Deferred loading: HTMX and Alpine.js loaded with `defer` attribute
|
||||
|
||||
## Template Architecture & Best Practices
|
||||
|
||||
### View Model Philosophy
|
||||
The application follows a **logic-in-Go, presentation-in-templates** approach:
|
||||
|
||||
- **View Models** (`viewmodels/issue_view.go`): Pre-process all business logic, calculations, and data transformations
|
||||
- **Templates**: Focus purely on presentation using pre-calculated data
|
||||
- **Helper Functions** (`templating/engine.go`): Reusable UI components and formatting
|
||||
|
||||
### Key View Model Features
|
||||
- **Pre-calculated metadata**: Page icons, grid layouts, visibility flags
|
||||
- **Grouped data structures**: Complex relationships resolved in Go
|
||||
- **Template helpers**: `PageIcon()`, `BeilagePageIcon()` for consistent UI components
|
||||
|
||||
### Template Organization
|
||||
**Ausgabe (Issue) Templates**:
|
||||
- `body.gohtml`: Main layout structure with conditional rendering
|
||||
- `components/_inhaltsverzeichnis.gohtml`: Table of contents with pre-processed page data
|
||||
- `components/_newspaper_layout.gohtml`: Newspaper page grid with absolute positioning
|
||||
- `components/_bilder.gohtml`: Simple image gallery fallback
|
||||
- Interactive highlighting system with intersection observer and scroll detection
|
||||
|
||||
### JavaScript Integration
|
||||
- **Progressive Enhancement**: HTMX + Alpine.js for interactivity
|
||||
- **Real-time Highlighting**: Intersection Observer API with scroll fallback
|
||||
- **Page Navigation**: Smooth scrolling with visibility detection
|
||||
- **Responsive Design**: Mobile-optimized with proper touch interactions
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Backend changes: Modify Go files, restart server
|
||||
2. Template changes: Edit templates in `views/`, automatic reload if watching enabled
|
||||
3. CSS changes: Run `npm run css` or `npm run tailwind` in views directory
|
||||
4. JavaScript changes: Edit `transform/main.js`, run `npm run build`
|
||||
5. Full rebuild: `go build` for backend, `npm run build` for frontend assets
|
||||
|
||||
### Adding New Template Logic
|
||||
1. **First**: Add business logic to view models in Go
|
||||
2. **Second**: Create reusable template helper functions if needed
|
||||
3. **Last**: Use pre-processed data in templates for presentation only
|
||||
@@ -1,39 +1,39 @@
|
||||
const p = "script[xslt-onload]", a = "xslt-template", u = "xslt-transformed", c = /* @__PURE__ */ new Map();
|
||||
function m() {
|
||||
let t = htmx.findAll(p);
|
||||
for (let e of t)
|
||||
T(e);
|
||||
const S = "script[xslt-onload]", w = "xslt-template", T = "xslt-transformed", f = /* @__PURE__ */ new Map();
|
||||
function b() {
|
||||
let n = htmx.findAll(S);
|
||||
for (let t of n)
|
||||
A(t);
|
||||
}
|
||||
function T(t) {
|
||||
if (t.getAttribute(u) === "true" || !t.hasAttribute(a))
|
||||
function A(n) {
|
||||
if (n.getAttribute(T) === "true" || !n.hasAttribute(w))
|
||||
return;
|
||||
let e = "#" + t.getAttribute(a), o = c.get(e);
|
||||
if (!o) {
|
||||
let n = htmx.find(e);
|
||||
if (n) {
|
||||
let l = n.innerHTML ? new DOMParser().parseFromString(n.innerHTML, "application/xml") : n.contentDocument;
|
||||
o = new XSLTProcessor(), o.importStylesheet(l), c.set(e, o);
|
||||
let t = "#" + n.getAttribute(w), e = f.get(t);
|
||||
if (!e) {
|
||||
let a = htmx.find(t);
|
||||
if (a) {
|
||||
let s = a.innerHTML ? new DOMParser().parseFromString(a.innerHTML, "application/xml") : a.contentDocument;
|
||||
e = new XSLTProcessor(), e.importStylesheet(s), f.set(t, e);
|
||||
} else
|
||||
throw new Error("Unknown XSLT template: " + e);
|
||||
throw new Error("Unknown XSLT template: " + t);
|
||||
}
|
||||
let i = new DOMParser().parseFromString(t.innerHTML, "application/xml"), s = o.transformToFragment(i, document), r = new XMLSerializer().serializeToString(s);
|
||||
t.outerHTML = r;
|
||||
let o = new DOMParser().parseFromString(n.innerHTML, "application/xml"), i = e.transformToFragment(o, document), r = new XMLSerializer().serializeToString(i);
|
||||
n.outerHTML = r;
|
||||
}
|
||||
function f() {
|
||||
document.querySelectorAll("template[simple]").forEach((e) => {
|
||||
let o = e.getAttribute("id"), i = e.content;
|
||||
function L() {
|
||||
document.querySelectorAll("template[simple]").forEach((t) => {
|
||||
let e = t.getAttribute("id"), o = t.content;
|
||||
customElements.define(
|
||||
o,
|
||||
e,
|
||||
class extends HTMLElement {
|
||||
constructor() {
|
||||
super(), this.appendChild(i.cloneNode(!0)), this.slots = this.querySelectorAll("slot");
|
||||
super(), this.appendChild(o.cloneNode(!0)), this.slots = this.querySelectorAll("slot");
|
||||
}
|
||||
connectedCallback() {
|
||||
let s = [];
|
||||
let i = [];
|
||||
this.slots.forEach((r) => {
|
||||
let n = r.getAttribute("name"), l = this.querySelector(`[slot="${n}"]`);
|
||||
l && (r.replaceWith(l.cloneNode(!0)), s.push(l));
|
||||
}), s.forEach((r) => {
|
||||
let a = r.getAttribute("name"), s = this.querySelector(`[slot="${a}"]`);
|
||||
s && (r.replaceWith(s.cloneNode(!0)), i.push(s));
|
||||
}), i.forEach((r) => {
|
||||
r.remove();
|
||||
});
|
||||
}
|
||||
@@ -41,11 +41,415 @@ function f() {
|
||||
);
|
||||
});
|
||||
}
|
||||
function d() {
|
||||
m(), htmx.on("htmx:load", function(t) {
|
||||
m();
|
||||
}), f();
|
||||
window.highlightObserver = window.highlightObserver || null;
|
||||
window.currentPageContainers = window.currentPageContainers || [];
|
||||
window.currentActiveIndex = window.currentActiveIndex || 0;
|
||||
window.pageObserver = window.pageObserver || null;
|
||||
window.scrollTimeout = window.scrollTimeout || null;
|
||||
function B() {
|
||||
window.highlightObserver && (window.highlightObserver.disconnect(), window.highlightObserver = null);
|
||||
const n = document.querySelectorAll(".newspaper-page-container");
|
||||
window.highlightObserver = new IntersectionObserver(
|
||||
(t) => {
|
||||
x();
|
||||
},
|
||||
{
|
||||
rootMargin: "-20% 0px -70% 0px"
|
||||
}
|
||||
), n.forEach((t) => {
|
||||
window.highlightObserver.observe(t);
|
||||
});
|
||||
}
|
||||
function x() {
|
||||
const n = [];
|
||||
document.querySelectorAll(".newspaper-page-container").forEach((e) => {
|
||||
const o = e.getBoundingClientRect(), i = window.innerHeight, r = Math.max(o.top, 0), a = Math.min(o.bottom, i), s = Math.max(0, a - r), l = o.height, g = s / l >= 0.5, u = e.querySelector("img[data-page]"), m = u ? u.getAttribute("data-page") : "unknown";
|
||||
g && u && m && !n.includes(m) && n.push(m);
|
||||
}), q(n), n.length > 0 && E(n);
|
||||
}
|
||||
function q(n) {
|
||||
document.querySelectorAll(".continuation-entry").forEach((t) => {
|
||||
t.style.display = "none";
|
||||
}), n.forEach((t) => {
|
||||
const e = document.querySelector(`[data-page-container="${t}"]`);
|
||||
e && e.querySelectorAll(".continuation-entry").forEach((i) => {
|
||||
i.style.display = "";
|
||||
});
|
||||
}), k(n), M();
|
||||
}
|
||||
function k(n) {
|
||||
document.querySelectorAll(".work-title").forEach((t) => {
|
||||
const e = t.getAttribute("data-short-title");
|
||||
e && (t.textContent = e);
|
||||
}), n.forEach((t) => {
|
||||
const e = document.querySelector(`[data-page-container="${t}"]`);
|
||||
e && e.querySelectorAll(".work-title").forEach((i) => {
|
||||
const r = i.getAttribute("data-full-title");
|
||||
r && r !== i.getAttribute("data-short-title") && (i.textContent = r);
|
||||
});
|
||||
});
|
||||
}
|
||||
function M() {
|
||||
document.querySelectorAll(".page-entry").forEach((n) => {
|
||||
const t = n.querySelectorAll(".inhalts-entry");
|
||||
let e = !1;
|
||||
t.forEach((o) => {
|
||||
window.getComputedStyle(o).display !== "none" && (e = !0);
|
||||
}), e ? n.style.display = "" : n.style.display = "none";
|
||||
});
|
||||
}
|
||||
function C(n) {
|
||||
E([n]);
|
||||
}
|
||||
function E(n) {
|
||||
console.log("markCurrentPagesInInhaltsverzeichnis called with:", n), document.querySelectorAll("[data-page-container]").forEach((e) => {
|
||||
e.hasAttribute("data-beilage") ? (e.classList.remove("border-red-500"), e.classList.add("border-amber-400")) : (e.classList.remove("border-red-500"), e.classList.add("border-slate-300"));
|
||||
}), document.querySelectorAll(".page-number-inhalts").forEach((e) => {
|
||||
e.classList.remove("text-red-600", "font-bold"), e.classList.add("text-slate-700", "font-semibold"), e.style.textDecoration = "", e.style.pointerEvents = "", e.classList.contains("bg-blue-50") ? e.classList.add("hover:bg-blue-100") : e.classList.contains("bg-amber-50") && e.classList.add("hover:bg-amber-100"), !e.classList.contains("bg-amber-50") && !e.classList.contains("bg-blue-50") && e.classList.add("bg-blue-50");
|
||||
}), document.querySelectorAll(".inhalts-entry").forEach((e) => {
|
||||
e.classList.add("hover:bg-slate-100"), e.style.cursor = "";
|
||||
}), document.querySelectorAll('.inhalts-entry a[href*="/"]').forEach((e) => {
|
||||
e.classList.remove("no-underline"), e.classList.contains("bg-blue-50") && e.classList.add("hover:bg-blue-100");
|
||||
});
|
||||
const t = [];
|
||||
n.forEach((e) => {
|
||||
const o = document.querySelector(
|
||||
`.page-number-inhalts[data-page-number="${e}"]`
|
||||
);
|
||||
if (o) {
|
||||
o.classList.remove(
|
||||
"text-slate-700",
|
||||
"hover:bg-blue-100",
|
||||
"hover:bg-amber-100"
|
||||
), o.classList.add("text-red-600", "font-bold"), o.style.textDecoration = "none", o.style.pointerEvents = "none", t.push(o);
|
||||
const i = document.querySelector(`[data-page-container="${e}"]`);
|
||||
i && (i.classList.remove("border-slate-300", "border-amber-400"), i.classList.add("border-red-500"));
|
||||
const r = o.closest(".page-entry");
|
||||
r && (r.querySelectorAll(".inhalts-entry").forEach((s) => {
|
||||
s.classList.remove("hover:bg-slate-100"), s.style.cursor = "default";
|
||||
}), r.querySelectorAll('a[href*="/"]').forEach((s) => {
|
||||
s.getAttribute("aria-current") === "page" && (s.style.textDecoration = "none", s.style.pointerEvents = "none", s.classList.add("no-underline"), s.classList.remove("hover:bg-blue-100"));
|
||||
}));
|
||||
}
|
||||
}), t.length > 0 && H(t[0]), document.querySelectorAll(".page-indicator").forEach((e) => {
|
||||
e.classList.remove("text-red-600", "font-bold"), e.classList.add("text-slate-600", "font-semibold"), e.classList.contains("bg-amber-50") || e.classList.add("bg-blue-50");
|
||||
}), n.forEach((e) => {
|
||||
const o = document.querySelector(`.page-indicator[data-page="${e}"]`);
|
||||
o && (o.classList.remove("text-slate-600"), o.classList.add("text-red-600", "font-bold"));
|
||||
});
|
||||
}
|
||||
function H(n) {
|
||||
const t = n.closest(".lg\\:overflow-y-auto");
|
||||
if (t) {
|
||||
const e = t.getBoundingClientRect(), o = n.getBoundingClientRect(), i = o.top < e.top, r = o.bottom > e.bottom;
|
||||
(i || r) && n.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center"
|
||||
});
|
||||
}
|
||||
}
|
||||
function R(n, t, e) {
|
||||
const o = document.getElementById("pageModal"), i = document.getElementById("modalImage");
|
||||
i.src = n.src, i.alt = n.alt, o.classList.remove("hidden"), C(t);
|
||||
}
|
||||
function I() {
|
||||
document.getElementById("pageModal").classList.add("hidden");
|
||||
}
|
||||
function $() {
|
||||
if (window.pageObserver && (window.pageObserver.disconnect(), window.pageObserver = null), window.currentPageContainers = Array.from(
|
||||
document.querySelectorAll(".newspaper-page-container")
|
||||
), window.currentActiveIndex = 0, h(), document.querySelector(".newspaper-page-container")) {
|
||||
let t = /* @__PURE__ */ new Set();
|
||||
window.pageObserver = new IntersectionObserver(
|
||||
(e) => {
|
||||
if (e.forEach((o) => {
|
||||
const i = window.currentPageContainers.indexOf(o.target);
|
||||
i !== -1 && (o.isIntersecting ? t.add(i) : t.delete(i));
|
||||
}), t.size > 0) {
|
||||
const i = Array.from(t).sort((r, a) => r - a)[0];
|
||||
i !== window.currentActiveIndex && (window.currentActiveIndex = i, h());
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: "-20% 0px -70% 0px"
|
||||
}
|
||||
), window.currentPageContainers.forEach((e) => {
|
||||
window.pageObserver.observe(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
function O() {
|
||||
if (window.currentActiveIndex > 0) {
|
||||
let n = -1;
|
||||
const t = [];
|
||||
window.currentPageContainers.forEach((o, i) => {
|
||||
const r = o.getBoundingClientRect(), a = window.innerHeight, s = Math.max(r.top, 0), l = Math.min(r.bottom, a), d = Math.max(0, l - s), g = r.height;
|
||||
d / g >= 0.3 && t.push(i);
|
||||
});
|
||||
const e = Math.min(...t);
|
||||
for (let o = e - 1; o >= 0; o--)
|
||||
if (!t.includes(o)) {
|
||||
n = o;
|
||||
break;
|
||||
}
|
||||
n === -1 && e > 0 && (n = e - 1), n >= 0 && (window.currentActiveIndex = n, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start"
|
||||
}), setTimeout(() => {
|
||||
h();
|
||||
}, 100));
|
||||
}
|
||||
}
|
||||
function K() {
|
||||
if (window.currentActiveIndex < window.currentPageContainers.length - 1) {
|
||||
let n = -1;
|
||||
const t = [];
|
||||
window.currentPageContainers.forEach((o, i) => {
|
||||
const r = o.getBoundingClientRect(), a = window.innerHeight, s = Math.max(r.top, 0), l = Math.min(r.bottom, a), d = Math.max(0, l - s), g = r.height;
|
||||
d / g >= 0.3 && t.push(i);
|
||||
});
|
||||
const e = Math.max(...t);
|
||||
for (let o = e + 1; o < window.currentPageContainers.length; o++)
|
||||
if (!t.includes(o)) {
|
||||
n = o;
|
||||
break;
|
||||
}
|
||||
n === -1 && e < window.currentPageContainers.length - 1 && (n = e + 1), n >= 0 && n < window.currentPageContainers.length && (window.currentActiveIndex = n, window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start"
|
||||
}), setTimeout(() => {
|
||||
h();
|
||||
}, 100));
|
||||
}
|
||||
}
|
||||
function V() {
|
||||
if (P()) {
|
||||
const t = document.querySelector("#newspaper-content .newspaper-page-container");
|
||||
t && t.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start"
|
||||
});
|
||||
} else {
|
||||
const t = document.querySelector('[class*="border-t-2 border-amber-200"]');
|
||||
t && t.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start"
|
||||
});
|
||||
}
|
||||
}
|
||||
function P() {
|
||||
const n = [];
|
||||
window.currentPageContainers.forEach((t, e) => {
|
||||
const o = t.getBoundingClientRect(), i = window.innerHeight, r = Math.max(o.top, 0), a = Math.min(o.bottom, i), s = Math.max(0, a - r), l = o.height;
|
||||
s / l >= 0.3 && n.push(e);
|
||||
});
|
||||
for (const t of n) {
|
||||
const e = window.currentPageContainers[t];
|
||||
if (e && e.id && e.id.includes("beilage-"))
|
||||
return !0;
|
||||
}
|
||||
return !1;
|
||||
}
|
||||
function h() {
|
||||
const n = document.getElementById("prevPageBtn"), t = document.getElementById("nextPageBtn"), e = document.getElementById("beilageBtn");
|
||||
if (n && (window.currentActiveIndex <= 0 ? n.style.display = "none" : n.style.display = "flex"), t && (window.currentActiveIndex >= window.currentPageContainers.length - 1 ? t.style.display = "none" : t.style.display = "flex"), e) {
|
||||
const o = P(), i = e.querySelector("i");
|
||||
o ? (e.title = "Zur Hauptausgabe", e.className = "w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 hover:text-gray-800 border border-gray-300 transition-colors duration-200 flex items-center justify-center cursor-pointer", i && (i.className = "ri-file-text-line text-lg lg:text-xl")) : (e.title = "Zu Beilage", e.className = "w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-amber-100 hover:bg-amber-200 text-amber-700 hover:text-amber-800 border border-amber-300 transition-colors duration-200 flex items-center justify-center cursor-pointer", i && (i.className = "ri-attachment-line text-lg lg:text-xl"));
|
||||
}
|
||||
}
|
||||
function D() {
|
||||
const n = document.getElementById("shareLinkBtn");
|
||||
let t = "";
|
||||
if (window.currentActiveIndex !== void 0 && window.currentPageContainers && window.currentPageContainers[window.currentActiveIndex]) {
|
||||
const i = window.currentPageContainers[window.currentActiveIndex].querySelector("[data-page]");
|
||||
i && (t = `#page-${i.getAttribute("data-page")}`);
|
||||
}
|
||||
const e = window.location.origin + window.location.pathname + t;
|
||||
navigator.share ? navigator.share({
|
||||
title: document.title,
|
||||
url: e
|
||||
}).catch((o) => {
|
||||
y(e, n);
|
||||
}) : y(e, n);
|
||||
}
|
||||
function y(n, t) {
|
||||
if (navigator.clipboard)
|
||||
navigator.clipboard.writeText(n).then(() => {
|
||||
c(t, "Link kopiert!");
|
||||
}).catch((e) => {
|
||||
c(t, "Kopieren fehlgeschlagen");
|
||||
});
|
||||
else {
|
||||
const e = document.createElement("textarea");
|
||||
e.value = n, document.body.appendChild(e), e.select();
|
||||
try {
|
||||
const o = document.execCommand("copy");
|
||||
c(t, o ? "Link kopiert!" : "Kopieren fehlgeschlagen");
|
||||
} catch {
|
||||
c(t, "Kopieren fehlgeschlagen");
|
||||
} finally {
|
||||
document.body.removeChild(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
function Z() {
|
||||
const n = document.getElementById("citationBtn"), t = document.title || "KGPZ", e = window.location.href, o = (/* @__PURE__ */ new Date()).toLocaleDateString("de-DE"), i = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${t}. Digital verfügbar unter: ${e} (Zugriff: ${o}).`;
|
||||
if (navigator.clipboard)
|
||||
navigator.clipboard.writeText(i).then(() => {
|
||||
c(n, "Zitation kopiert!");
|
||||
}).catch((r) => {
|
||||
c(n, "Kopieren fehlgeschlagen");
|
||||
});
|
||||
else {
|
||||
const r = document.createElement("textarea");
|
||||
r.value = i, document.body.appendChild(r), r.select();
|
||||
try {
|
||||
const a = document.execCommand("copy");
|
||||
c(n, a ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
|
||||
} catch {
|
||||
c(n, "Kopieren fehlgeschlagen");
|
||||
} finally {
|
||||
document.body.removeChild(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
function c(n, t) {
|
||||
const e = document.querySelector(".simple-popup");
|
||||
e && e.remove();
|
||||
const o = document.createElement("div");
|
||||
o.className = "simple-popup", o.textContent = t, o.style.cssText = `
|
||||
position: fixed;
|
||||
background: #374151;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
const i = n.getBoundingClientRect(), r = window.innerHeight, a = window.innerWidth;
|
||||
let s = i.left - 10, l = i.bottom + 8;
|
||||
const d = 120, g = 32;
|
||||
s + d > a && (s = i.right - d + 10), l + g > r && (l = i.top - g - 8), o.style.left = Math.max(5, s) + "px", o.style.top = Math.max(5, l) + "px", document.body.appendChild(o), setTimeout(() => {
|
||||
o.style.opacity = "1";
|
||||
}, 10), setTimeout(() => {
|
||||
o.style.opacity = "0", setTimeout(() => {
|
||||
o.parentNode && o.remove();
|
||||
}, 200);
|
||||
}, 2e3);
|
||||
}
|
||||
function v() {
|
||||
const n = window.location.hash;
|
||||
let t = "", e = null;
|
||||
if (n.startsWith("#page-")) {
|
||||
if (t = n.replace("#page-", ""), e = document.getElementById(`page-${t}`), !e) {
|
||||
const o = document.querySelectorAll(".newspaper-page-container[data-pages]");
|
||||
for (const i of o) {
|
||||
const r = i.getAttribute("data-pages");
|
||||
if (r && r.split(",").includes(t)) {
|
||||
e = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
e || (e = document.getElementById(`beilage-1-page-${t}`) || document.getElementById(`beilage-2-page-${t}`) || document.querySelector(`[id*="beilage"][id*="page-${t}"]`));
|
||||
} else if (n.startsWith("#beilage-")) {
|
||||
const o = n.match(/#beilage-(\d+)-page-(\d+)/);
|
||||
if (o) {
|
||||
const i = o[1];
|
||||
t = o[2], e = document.getElementById(`beilage-${i}-page-${t}`);
|
||||
}
|
||||
}
|
||||
e && t && setTimeout(() => {
|
||||
e.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start"
|
||||
}), C(t);
|
||||
}, 300);
|
||||
}
|
||||
function z(n, t, e = !1) {
|
||||
let o = "";
|
||||
e ? o = `#beilage-1-page-${n}` : o = `#page-${n}`;
|
||||
const i = window.location.origin + window.location.pathname + o;
|
||||
if (navigator.clipboard)
|
||||
navigator.clipboard.writeText(i).then(() => {
|
||||
c(t, "Link kopiert!");
|
||||
}).catch((r) => {
|
||||
c(t, "Kopieren fehlgeschlagen");
|
||||
});
|
||||
else {
|
||||
const r = document.createElement("textarea");
|
||||
r.value = i, document.body.appendChild(r), r.select();
|
||||
try {
|
||||
const a = document.execCommand("copy");
|
||||
c(t, a ? "Link kopiert!" : "Kopieren fehlgeschlagen");
|
||||
} catch {
|
||||
c(t, "Kopieren fehlgeschlagen");
|
||||
} finally {
|
||||
document.body.removeChild(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
function _(n, t) {
|
||||
const e = document.title || "KGPZ", o = `${window.location.origin}${window.location.pathname}#page-${n}`, i = (/* @__PURE__ */ new Date()).toLocaleDateString("de-DE"), r = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${e}, Seite ${n}. Digital verfügbar unter: ${o} (Zugriff: ${i}).`;
|
||||
if (navigator.clipboard)
|
||||
navigator.clipboard.writeText(r).then(() => {
|
||||
c(t, "Zitation kopiert!");
|
||||
}).catch((a) => {
|
||||
c(t, "Kopieren fehlgeschlagen");
|
||||
});
|
||||
else {
|
||||
const a = document.createElement("textarea");
|
||||
a.value = r, document.body.appendChild(a), a.select();
|
||||
try {
|
||||
const s = document.execCommand("copy");
|
||||
c(t, s ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
|
||||
} catch {
|
||||
c(t, "Kopieren fehlgeschlagen");
|
||||
} finally {
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
function p() {
|
||||
B(), $(), window.addEventListener("scroll", function() {
|
||||
clearTimeout(window.scrollTimeout), window.scrollTimeout = setTimeout(() => {
|
||||
x(), h();
|
||||
}, 50);
|
||||
}), v(), window.addEventListener("hashchange", v), document.addEventListener("keydown", function(n) {
|
||||
n.key === "Escape" && I();
|
||||
});
|
||||
}
|
||||
window.enlargePage = R;
|
||||
window.closeModal = I;
|
||||
window.scrollToPreviousPage = O;
|
||||
window.scrollToNextPage = K;
|
||||
window.scrollToBeilage = V;
|
||||
window.shareCurrentPage = D;
|
||||
window.generateCitation = Z;
|
||||
window.copyPagePermalink = z;
|
||||
window.generatePageCitation = _;
|
||||
function F() {
|
||||
b(), L(), document.querySelector(".newspaper-page-container") && p(), htmx.on("htmx:load", function(n) {
|
||||
b();
|
||||
}), document.body.addEventListener("htmx:afterSwap", function(n) {
|
||||
setTimeout(() => {
|
||||
document.querySelector(".newspaper-page-container") && p();
|
||||
}, 100);
|
||||
}), document.body.addEventListener("htmx:afterSettle", function(n) {
|
||||
setTimeout(() => {
|
||||
document.querySelector(".newspaper-page-container") && p();
|
||||
}, 200);
|
||||
}), document.body.addEventListener("htmx:load", function(n) {
|
||||
setTimeout(() => {
|
||||
document.querySelector(".newspaper-page-container") && p();
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
export {
|
||||
d as setup
|
||||
F as setup
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,41 +0,0 @@
|
||||
{{ $model := .model }}
|
||||
{{ $images := $model.Images }}
|
||||
{{ if $images.HasImages }}
|
||||
<div class="mt-6">
|
||||
<h3 class="text-lg font-medium mb-4">Seiten der Ausgabe</h3>
|
||||
|
||||
{{- if $images.MainPages }}
|
||||
<div class="mb-6">
|
||||
<h4 class="text-md font-medium mb-2">Hauptausgabe</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{- range $images.MainPages -}}
|
||||
{{- if .Available -}}
|
||||
<div class="border rounded-lg p-2">
|
||||
<h5 class="font-bold text-slate-700 bg-blue-50 px-2 py-1 rounded text-sm mb-2">Seite {{ .PageNumber }}</h5>
|
||||
<img src="{{ .ImagePath }}" alt="Seite {{ .PageNumber }}" class="w-full h-auto border" loading="lazy">
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
|
||||
{{- range $beilageNum, $pages := $images.AdditionalPages -}}
|
||||
{{- if $pages }}
|
||||
<div class="mb-6">
|
||||
<h4 class="text-md font-medium mb-2">Beilage {{ $beilageNum }}</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{- range $pages -}}
|
||||
{{- if .Available -}}
|
||||
<div class="border rounded-lg p-2">
|
||||
<h5 class="font-bold text-slate-700 bg-blue-50 px-2 py-1 rounded text-sm mb-2">Seite {{ .PageNumber }}</h5>
|
||||
<img src="{{ .ImagePath }}" alt="Beilage {{ $beilageNum }}, Seite {{ .PageNumber }}" class="w-full h-auto border" loading="lazy">
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
</div>
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,579 +0,0 @@
|
||||
{{ $model := .model }}
|
||||
|
||||
<div class="w-full min-h-screen">
|
||||
<!-- Navigation Bar -->
|
||||
<div class="sticky top-0 z-40 bg-white border-b border-gray-200 shadow-sm mb-6">
|
||||
<div class="max-w-7xl mx-auto px-4 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
{{ template "_title_nav" . }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Navigation buttons -->
|
||||
<button onclick="scrollToPreviousPage()" class="flex items-center px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
|
||||
<i class="ri-arrow-up-line mr-1"></i>
|
||||
Vorherige Seite
|
||||
</button>
|
||||
<button onclick="scrollToNextPage()" class="flex items-center px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors">
|
||||
<i class="ri-arrow-down-line mr-1"></i>
|
||||
Nächste Seite
|
||||
</button>
|
||||
{{ if $model.Images.AdditionalPages }}
|
||||
<button onclick="scrollToBeilage()" class="flex items-center px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white rounded-md transition-colors">
|
||||
<i class="ri-attachment-line mr-1"></i>
|
||||
Zu Beilage
|
||||
</button>
|
||||
{{ end }}
|
||||
<!-- Switch back to sidebar layout -->
|
||||
<a href="?layout=sidebar" class="flex items-center px-3 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-md transition-colors">
|
||||
<i class="ri-sidebar-unfold-line mr-1"></i>
|
||||
Seitenleiste
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4">
|
||||
<div class="flex flex-col lg:flex-row gap-6 w-full">
|
||||
<!-- Left side: Collapsible Inhaltsverzeichnis -->
|
||||
<div class="lg:w-1/4 xl:w-1/5 flex-shrink-0">
|
||||
<div class="lg:sticky lg:top-24 lg:max-h-[calc(100vh-6rem)] lg:overflow-y-auto">
|
||||
{{ template "_inhaltsverzeichnis" . }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right side: Full-width Newspaper pages -->
|
||||
<div class="lg:w-3/4 xl:w-4/5 flex-1">
|
||||
{{ template "_newspaper_layout" . }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPageContainers = [];
|
||||
let currentActiveIndex = 0;
|
||||
|
||||
// Initialize page tracking for full-width layout
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get all page containers
|
||||
currentPageContainers = document.querySelectorAll('.newspaper-page-container');
|
||||
|
||||
// Set up intersection observer for active page tracking
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
// Check if this is a double-spread container
|
||||
const doubleSpread = entry.target.querySelector('.double-spread');
|
||||
if (doubleSpread) {
|
||||
// Handle double-spread: highlight both pages
|
||||
const pageImages = entry.target.querySelectorAll('img[data-page]');
|
||||
const pageNumbers = Array.from(pageImages).map(img => img.getAttribute('data-page'));
|
||||
markCurrentPagesInInhaltsverzeichnis(pageNumbers);
|
||||
} else {
|
||||
// Handle single page
|
||||
const pageImg = entry.target.querySelector('img[data-page]');
|
||||
if (pageImg) {
|
||||
const pageNumber = pageImg.getAttribute('data-page');
|
||||
markCurrentPageInInhaltsverzeichnis(pageNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// Update current active index
|
||||
const containerIndex = Array.from(currentPageContainers).indexOf(entry.target);
|
||||
if (containerIndex !== -1) {
|
||||
currentActiveIndex = containerIndex;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, {
|
||||
rootMargin: '-20% 0px -70% 0px' // Trigger when page is mostly in view
|
||||
});
|
||||
|
||||
// Observe all page containers
|
||||
currentPageContainers.forEach(container => {
|
||||
observer.observe(container);
|
||||
});
|
||||
});
|
||||
|
||||
function scrollToPreviousPage() {
|
||||
if (currentActiveIndex > 0) {
|
||||
currentActiveIndex--;
|
||||
currentPageContainers[currentActiveIndex].scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToNextPage() {
|
||||
if (currentActiveIndex < currentPageContainers.length - 1) {
|
||||
currentActiveIndex++;
|
||||
currentPageContainers[currentActiveIndex].scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBeilage() {
|
||||
const beilageElement = document.querySelector('[id^="beilage-"]');
|
||||
if (beilageElement) {
|
||||
beilageElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function markCurrentPageInInhaltsverzeichnis(pageNumber) {
|
||||
markCurrentPagesInInhaltsverzeichnis([pageNumber]);
|
||||
}
|
||||
|
||||
function markCurrentPagesInInhaltsverzeichnis(pageNumbers) {
|
||||
// Reset all page container borders to default
|
||||
document.querySelectorAll('[data-page-container]').forEach(container => {
|
||||
if (container.hasAttribute('data-beilage')) {
|
||||
container.classList.remove('border-red-500');
|
||||
container.classList.add('border-amber-400');
|
||||
} else {
|
||||
container.classList.remove('border-red-500');
|
||||
container.classList.add('border-slate-300');
|
||||
}
|
||||
});
|
||||
|
||||
// Hide all continuation entries by default
|
||||
document.querySelectorAll('.continuation-entry').forEach(entry => {
|
||||
entry.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Reset all page numbers in Inhaltsverzeichnis
|
||||
document.querySelectorAll('.page-number-inhalts').forEach(pageNum => {
|
||||
pageNum.classList.remove('bg-red-500', 'text-white');
|
||||
pageNum.classList.add('text-slate-700');
|
||||
pageNum.style.textDecoration = '';
|
||||
pageNum.style.pointerEvents = '';
|
||||
// Restore hover effects
|
||||
if (pageNum.classList.contains('bg-blue-50')) {
|
||||
pageNum.classList.add('hover:bg-blue-100');
|
||||
} else if (pageNum.classList.contains('bg-amber-50')) {
|
||||
pageNum.classList.add('hover:bg-amber-100');
|
||||
}
|
||||
// Restore original background colors
|
||||
if (pageNum.classList.contains('bg-amber-50')) {
|
||||
// Keep amber background for Beilage pages
|
||||
} else {
|
||||
pageNum.classList.remove('bg-amber-50');
|
||||
pageNum.classList.add('bg-blue-50');
|
||||
}
|
||||
});
|
||||
|
||||
// Reset all containers and links in Inhaltsverzeichnis
|
||||
document.querySelectorAll('.inhalts-entry').forEach(container => {
|
||||
container.classList.add('hover:bg-slate-100');
|
||||
container.style.cursor = '';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.inhalts-entry .author-link').forEach(link => {
|
||||
link.style.textDecoration = '';
|
||||
link.style.pointerEvents = '';
|
||||
link.classList.remove('no-underline');
|
||||
});
|
||||
|
||||
document.querySelectorAll('.inhalts-entry a[href*="/"]').forEach(link => {
|
||||
link.style.textDecoration = '';
|
||||
link.style.pointerEvents = '';
|
||||
link.classList.remove('no-underline');
|
||||
if (link.classList.contains('bg-blue-50')) {
|
||||
link.classList.add('hover:bg-blue-100');
|
||||
}
|
||||
});
|
||||
|
||||
// Find and highlight the current page numbers
|
||||
const highlightedElements = [];
|
||||
const highlightedRanges = new Set(); // Track which ranges we've already highlighted
|
||||
|
||||
pageNumbers.forEach(pageNumber => {
|
||||
// Convert pageNumber to integer for comparison
|
||||
const currentPageNum = parseInt(pageNumber);
|
||||
// Look for all entries that should be highlighted for this page
|
||||
const allPageNumbers = document.querySelectorAll('.page-number-inhalts');
|
||||
|
||||
for (const pageNumElement of allPageNumbers) {
|
||||
const startPage = parseInt(pageNumElement.getAttribute('data-page-number'));
|
||||
const endPage = parseInt(pageNumElement.getAttribute('data-end-page'));
|
||||
const rangeKey = `${startPage}-${endPage}`;
|
||||
|
||||
// Check if this page falls within this range
|
||||
if (currentPageNum >= startPage && currentPageNum <= endPage) {
|
||||
// Only highlight this range once, even if multiple visible pages fall within it
|
||||
if (!highlightedRanges.has(rangeKey)) {
|
||||
pageNumElement.classList.remove('bg-blue-50', 'bg-amber-50', 'text-slate-700', 'hover:bg-blue-100', 'hover:bg-amber-100');
|
||||
pageNumElement.classList.add('bg-red-500', 'text-white');
|
||||
pageNumElement.style.textDecoration = 'none';
|
||||
pageNumElement.style.pointerEvents = 'none';
|
||||
highlightedElements.push(pageNumElement);
|
||||
highlightedRanges.add(rangeKey);
|
||||
|
||||
// Highlight the page container's left border
|
||||
const pageContainer = document.querySelector(`[data-page-container="${startPage}"]`);
|
||||
if (pageContainer) {
|
||||
pageContainer.classList.remove('border-slate-300', 'border-amber-400');
|
||||
pageContainer.classList.add('border-red-500');
|
||||
|
||||
// Show continuation entries for this visible page
|
||||
const continuationEntries = pageContainer.querySelectorAll('.continuation-entry[data-page="' + startPage + '"]');
|
||||
continuationEntries.forEach(entry => {
|
||||
entry.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
// Also make links in the current article non-clickable and remove hover effects
|
||||
const parentEntry = pageNumElement.closest('.mb-4');
|
||||
if (parentEntry) {
|
||||
// Remove hover effects from the container
|
||||
const entryContainers = parentEntry.querySelectorAll('.inhalts-entry');
|
||||
entryContainers.forEach(container => {
|
||||
container.classList.remove('hover:bg-slate-100');
|
||||
container.style.cursor = 'default';
|
||||
});
|
||||
|
||||
// Make all links non-clickable and remove underlines
|
||||
parentEntry.querySelectorAll('.author-link').forEach(link => {
|
||||
link.style.textDecoration = 'none';
|
||||
link.style.pointerEvents = 'none';
|
||||
link.classList.add('no-underline');
|
||||
});
|
||||
|
||||
// Also handle issue reference links
|
||||
parentEntry.querySelectorAll('a[href*="/"]').forEach(link => {
|
||||
if (link.getAttribute('aria-current') === 'page') {
|
||||
link.style.textDecoration = 'none';
|
||||
link.style.pointerEvents = 'none';
|
||||
link.classList.add('no-underline');
|
||||
link.classList.remove('hover:bg-blue-100');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-scroll to first highlighted element if it exists
|
||||
if (highlightedElements.length > 0) {
|
||||
scrollToHighlightedPage(highlightedElements[0]);
|
||||
}
|
||||
|
||||
// Also highlight page indicators
|
||||
document.querySelectorAll('.page-indicator').forEach(indicator => {
|
||||
indicator.classList.remove('bg-red-500', 'text-white');
|
||||
indicator.classList.add('bg-blue-50', 'text-slate-600');
|
||||
});
|
||||
|
||||
// Highlight page indicators for all current pages
|
||||
pageNumbers.forEach(pageNumber => {
|
||||
const pageIndicator = document.querySelector(`.page-indicator[data-page="${pageNumber}"]`);
|
||||
if (pageIndicator) {
|
||||
pageIndicator.classList.remove('bg-blue-50', 'bg-green-50', 'bg-amber-50', 'text-slate-600');
|
||||
pageIndicator.classList.add('bg-red-500', 'text-white');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scrollToHighlightedPage(element) {
|
||||
// Check if the element is in a scrollable container
|
||||
const inhaltsContainer = element.closest('.lg\\:overflow-y-auto');
|
||||
if (inhaltsContainer) {
|
||||
// Calculate position
|
||||
const containerRect = inhaltsContainer.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
// Check if element is not fully visible
|
||||
const isAboveContainer = elementRect.top < containerRect.top;
|
||||
const isBelowContainer = elementRect.bottom > containerRect.bottom;
|
||||
|
||||
if (isAboveContainer || isBelowContainer) {
|
||||
// Scroll to make element visible with some padding
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
// Close modal if open
|
||||
const modal = document.getElementById('pageModal');
|
||||
if (modal && !modal.classList.contains('hidden')) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
} else if (e.key === 'ArrowUp' || e.key === 'PageUp') {
|
||||
e.preventDefault();
|
||||
scrollToPreviousPage();
|
||||
} else if (e.key === 'ArrowDown' || e.key === 'PageDown') {
|
||||
e.preventDefault();
|
||||
scrollToNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
function shareCurrentPage() {
|
||||
const button = document.getElementById('shareLinkBtn');
|
||||
|
||||
// Get current page information
|
||||
let pageInfo = '';
|
||||
|
||||
// Try to get the currently visible page number from active containers
|
||||
if (window.currentActiveIndex !== undefined && window.currentPageContainers && window.currentPageContainers[window.currentActiveIndex]) {
|
||||
const activeContainer = window.currentPageContainers[window.currentActiveIndex];
|
||||
const pageElement = activeContainer.querySelector('[data-page]');
|
||||
if (pageElement) {
|
||||
const pageNumber = pageElement.getAttribute('data-page');
|
||||
pageInfo = `#page-${pageNumber}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the shareable URL
|
||||
const currentUrl = window.location.origin + window.location.pathname + pageInfo;
|
||||
|
||||
// Try to use Web Share API if available (mobile browsers)
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: document.title,
|
||||
url: currentUrl
|
||||
}).catch(err => {
|
||||
console.log('Error sharing:', err);
|
||||
// Fallback to clipboard
|
||||
copyToClipboard(currentUrl, button);
|
||||
});
|
||||
} else {
|
||||
// Fallback: copy to clipboard
|
||||
copyToClipboard(currentUrl, button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function copyToClipboard(text, button) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showSimplePopup(button, 'Link kopiert!');
|
||||
}).catch(err => {
|
||||
showSimplePopup(button, 'Kopieren fehlgeschlagen');
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
showSimplePopup(button, successful ? 'Link kopiert!' : 'Kopieren fehlgeschlagen');
|
||||
} catch (err) {
|
||||
showSimplePopup(button, 'Kopieren fehlgeschlagen');
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateCitation() {
|
||||
const button = document.getElementById('citationBtn');
|
||||
|
||||
// Get current page and issue information
|
||||
const issueInfo = document.title || 'KGPZ';
|
||||
const currentUrl = window.location.href;
|
||||
|
||||
// Basic citation format (can be expanded later)
|
||||
const currentDate = new Date().toLocaleDateString('de-DE');
|
||||
const citation = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${issueInfo}. Digital verfügbar unter: ${currentUrl} (Zugriff: ${currentDate}).`;
|
||||
|
||||
// Copy to clipboard
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(citation).then(() => {
|
||||
showSimplePopup(button, 'Zitation kopiert!');
|
||||
}).catch(err => {
|
||||
showSimplePopup(button, 'Kopieren fehlgeschlagen');
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = citation;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
showSimplePopup(button, successful ? 'Zitation kopiert!' : 'Kopieren fehlgeschlagen');
|
||||
} catch (err) {
|
||||
showSimplePopup(button, 'Kopieren fehlgeschlagen');
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showSimplePopup(button, message) {
|
||||
// Remove any existing popup
|
||||
const existingPopup = document.querySelector('.simple-popup');
|
||||
if (existingPopup) {
|
||||
existingPopup.remove();
|
||||
}
|
||||
|
||||
// Create popup element
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'simple-popup';
|
||||
popup.textContent = message;
|
||||
|
||||
// Style the popup
|
||||
popup.style.cssText = `
|
||||
position: fixed;
|
||||
background: #374151;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
// Position popup next to button
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
||||
|
||||
popup.style.left = (buttonRect.left + scrollLeft - 10) + 'px';
|
||||
popup.style.top = (buttonRect.bottom + scrollTop + 8) + 'px';
|
||||
|
||||
// Add to page
|
||||
document.body.appendChild(popup);
|
||||
|
||||
// Fade in
|
||||
setTimeout(() => {
|
||||
popup.style.opacity = '1';
|
||||
}, 10);
|
||||
|
||||
// Auto-remove after 2 seconds
|
||||
setTimeout(() => {
|
||||
popup.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (popup.parentNode) {
|
||||
popup.remove();
|
||||
}
|
||||
}, 200);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Function to copy page permalink
|
||||
function copyPagePermalink(pageNumber, button, isBeilage = false) {
|
||||
let pageFragment = '';
|
||||
if (isBeilage) {
|
||||
pageFragment = `#beilage-1-page-${pageNumber}`;
|
||||
} else {
|
||||
pageFragment = `#page-${pageNumber}`;
|
||||
}
|
||||
|
||||
const currentUrl = window.location.origin + window.location.pathname + pageFragment;
|
||||
|
||||
// Copy to clipboard
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(currentUrl).then(() => {
|
||||
showSimplePopup(button, 'Link kopiert!');
|
||||
}).catch(err => {
|
||||
showSimplePopup(button, 'Kopieren fehlgeschlagen');
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = currentUrl;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
showSimplePopup(button, successful ? 'Link kopiert!' : 'Kopieren fehlgeschlagen');
|
||||
} catch (err) {
|
||||
showSimplePopup(button, 'Kopieren fehlgeschlagen');
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle hash navigation to scroll to specific pages
|
||||
function scrollToPageFromHash() {
|
||||
const hash = window.location.hash;
|
||||
let pageNumber = '';
|
||||
let targetContainer = null;
|
||||
|
||||
if (hash.startsWith('#page-')) {
|
||||
pageNumber = hash.replace('#page-', '');
|
||||
|
||||
// Try to find exact page container first
|
||||
targetContainer = document.getElementById(`page-${pageNumber}`);
|
||||
|
||||
// If not found, try to find container that contains this page
|
||||
if (!targetContainer) {
|
||||
// Look for double-spread containers that contain this page
|
||||
const containers = document.querySelectorAll('.newspaper-page-container[data-pages]');
|
||||
for (const container of containers) {
|
||||
const pages = container.getAttribute('data-pages');
|
||||
if (pages && pages.split(',').includes(pageNumber)) {
|
||||
targetContainer = container;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still not found, try beilage containers
|
||||
if (!targetContainer) {
|
||||
targetContainer = document.getElementById(`beilage-1-page-${pageNumber}`) ||
|
||||
document.getElementById(`beilage-2-page-${pageNumber}`) ||
|
||||
document.querySelector(`[id*="beilage"][id*="page-${pageNumber}"]`);
|
||||
}
|
||||
} else if (hash.startsWith('#beilage-')) {
|
||||
// Handle beilage-specific hashes like #beilage-1-page-101
|
||||
const match = hash.match(/#beilage-(\d+)-page-(\d+)/);
|
||||
if (match) {
|
||||
const beilageNum = match[1];
|
||||
pageNumber = match[2];
|
||||
targetContainer = document.getElementById(`beilage-${beilageNum}-page-${pageNumber}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetContainer && pageNumber) {
|
||||
setTimeout(() => {
|
||||
targetContainer.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
|
||||
// Highlight the specific page in the table of contents
|
||||
markCurrentPageInInhaltsverzeichnis(pageNumber);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to copy page permalink
|
||||
function copyPagePermalink(pageNumber, button, isBeilage = false) {
|
||||
let pageFragment = '';
|
||||
if (isBeilage) {
|
||||
pageFragment = `#beilage-1-page-${pageNumber}`;
|
||||
} else {
|
||||
pageFragment = `#page-${pageNumber}`;
|
||||
}
|
||||
|
||||
const currentUrl = window.location.origin + window.location.pathname + pageFragment;
|
||||
copyToClipboardWithMessage(currentUrl, button, 'Link kopiert!');
|
||||
}
|
||||
|
||||
// Initialize hash handling
|
||||
document.addEventListener('DOMContentLoaded', scrollToPageFromHash);
|
||||
window.addEventListener('hashchange', scrollToPageFromHash);
|
||||
</script>
|
||||
@@ -13,13 +13,20 @@
|
||||
{{ $firstItem := "" }}
|
||||
{{ if $pageItems }}{{ $firstItem = index $pageItems 0 }}{{ end }}
|
||||
|
||||
|
||||
<!-- Individual page entry -->
|
||||
<div
|
||||
class="mb-6 first:mb-0 pl-4 border-l-4 border-slate-300 page-entry"
|
||||
data-page-container="{{ $page }}">
|
||||
<div class="flex items-center justify-between gap-2 mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-container">{{ if $firstItem }}{{ PageIcon $firstItem.PageIcon }}{{ else }}{{ PageIcon "first" }}{{ end }}</span>
|
||||
<span class="icon-container"
|
||||
>{{ if $firstItem }}
|
||||
{{ PageIcon $firstItem.PageIcon }}
|
||||
{{ else }}
|
||||
{{ PageIcon "first" }}
|
||||
{{ end }}</span
|
||||
>
|
||||
<a
|
||||
href="#page-{{ $page }}"
|
||||
class="page-number-inhalts font-bold text-slate-700 bg-slate-100 px-2 py-1 rounded text-sm transition-colors duration-200 hover:bg-slate-200 no-underline"
|
||||
@@ -40,50 +47,68 @@
|
||||
<div class="space-y-0">
|
||||
{{ if $pageItems }}
|
||||
{{ range $individualPiece := $pageItems }}
|
||||
<div
|
||||
class="inhalts-entry py-1 px-0 bg-slate-50 rounded hover:bg-slate-100 transition-colors duration-200 {{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
continuation-entry
|
||||
{{ end }}"
|
||||
data-page="{{ $page }}"
|
||||
{{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
data-is-continuation="true"
|
||||
{{ end }}
|
||||
style="{{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
display: none;
|
||||
{{ end }}">
|
||||
{{ template "_inhaltsverzeichnis_eintrag" $individualPiece.PieceByIssue }}
|
||||
<div
|
||||
class="inhalts-entry py-1 px-0 bg-slate-50 rounded hover:bg-slate-100 transition-colors duration-200 {{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
continuation-entry
|
||||
{{ end }}"
|
||||
data-page="{{ $page }}"
|
||||
{{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
data-is-continuation="true"
|
||||
{{ end }}
|
||||
style="{{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
display: none;
|
||||
{{ end }}">
|
||||
{{ template "_inhaltsverzeichnis_eintrag" $individualPiece.PieceByIssue }}
|
||||
|
||||
|
||||
<!-- Links zu anderen Teilen: -->
|
||||
{{ if and (not $individualPiece.PieceByIssue.IsContinuation) (gt (len $individualPiece.IssueRefs) 1) }}
|
||||
<div class="mt-1 pt-1 border-t border-slate-100">
|
||||
<div class="flex items-center flex-wrap gap-1">
|
||||
<i class="ri-links-line text-slate-600 text-sm mr-1"></i>
|
||||
{{ range $issue := $individualPiece.IssueRefs }}
|
||||
<a
|
||||
href="/{{- $issue.When -}}/{{- $issue.Nr -}}{{- if $issue.Von -}}
|
||||
{{- if $issue.Beilage -}}
|
||||
#beilage-{{ $issue.Beilage }}-page-{{ $issue.Von }}
|
||||
{{- else -}}
|
||||
#page-{{ $issue.Von }}
|
||||
{{- end -}}
|
||||
{{- end -}}"
|
||||
class="inline-flex items-center gap-1 px-2 py-1 bg-slate-100 text-slate-700 rounded-md text-xs font-medium hover:bg-slate-200 transition-colors duration-150"
|
||||
{{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year
|
||||
$model.Datum.When.Year)
|
||||
-}}
|
||||
aria-current="page"
|
||||
{{ end }}>
|
||||
<i class="ri-calendar-line text-xs"></i>
|
||||
{{- $issue.When.Year }} Nr.
|
||||
{{ $issue.Nr -}}
|
||||
</a>
|
||||
{{ end }}
|
||||
<!-- Links zu anderen Teilen: -->
|
||||
{{ if and (not $individualPiece.PieceByIssue.IsContinuation) (gt (len $individualPiece.IssueRefs) 1) }}
|
||||
<div class="mt-1 pt-1 border-t border-slate-100">
|
||||
<div class="flex items-center flex-wrap gap-1">
|
||||
<i class="ri-links-line text-slate-600 text-sm mr-1"></i>
|
||||
{{ range $index, $issue := $individualPiece.IssueRefs }}
|
||||
<div
|
||||
class="inline-flex items-center gap-1 px-2 py-1 bg-slate-100 rounded-md text-xs font-medium hover:bg-slate-200 transition-colors duration-150 {{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year $model.Datum.When.Year) -}}
|
||||
bg-red-100 text-red-700
|
||||
{{ end }}">
|
||||
<span class="text-black text-xs font-bold">{{ add $index 1 }}</span>
|
||||
<span class="w-px h-3 bg-slate-300 mx-1"></span>
|
||||
<a
|
||||
href="/{{- $issue.When -}}/{{- $issue.Nr -}}{{- if $issue.Von -}}
|
||||
{{- if $issue.Beilage -}}
|
||||
#beilage-{{ $issue.Beilage }}-page-{{ $issue.Von }}
|
||||
{{- else -}}
|
||||
#page-{{ $issue.Von }}
|
||||
{{- end -}}
|
||||
{{- end -}}"
|
||||
class="text-slate-700 no-underline hover:text-slate-900 {{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year $model.Datum.When.Year) -}}
|
||||
text-red-700 pointer-events-none
|
||||
{{ end }}"
|
||||
{{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year
|
||||
$model.Datum.When.Year)
|
||||
-}}
|
||||
aria-current="page"
|
||||
{{ end }}>
|
||||
{{- $issueKey := printf "%d-%d" $issue.When.Year $issue.Nr -}}
|
||||
{{- $issueData := GetIssue $issueKey -}}
|
||||
{{- if $issueData -}}
|
||||
{{ $issueData.Datum.When.Day }}.{{ $issueData.Datum.When.Month }}.{{ $issueData.Datum.When.Year }}
|
||||
{{- else -}}
|
||||
{{ $issue.When.Year }} Nr.
|
||||
{{ $issue.Nr }}
|
||||
{{- end -}}
|
||||
{{- if $issue.Von }}
|
||||
S.
|
||||
{{ $issue.Von }}
|
||||
{{- end -}}
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<!-- Empty page indicator -->
|
||||
<div class="inhalts-entry py-1 px-0">
|
||||
@@ -113,10 +138,16 @@
|
||||
{{ $allPages := $model.AdditionalPieces.Pages }}
|
||||
{{ $pageCount := len $allPages }}
|
||||
{{ $iconType := "first" }}
|
||||
{{ if eq $pageIndex 0 }}{{ $iconType = "first" }}
|
||||
{{ else if eq $pageIndex (sub $pageCount 1) }}{{ $iconType = "last" }}
|
||||
{{ else if eq (mod $pageIndex 2) 1 }}{{ $iconType = "even" }}
|
||||
{{ else }}{{ $iconType = "odd" }}{{ end }}
|
||||
{{ if eq $pageIndex 0 }}
|
||||
{{ $iconType = "first" }}
|
||||
{{ else if eq $pageIndex (sub $pageCount 1) }}
|
||||
{{ $iconType = "last" }}
|
||||
{{ else if eq (mod $pageIndex 2) 1 }}
|
||||
{{ $iconType = "even" }}
|
||||
{{ else }}
|
||||
{{ $iconType = "odd" }}
|
||||
{{ end }}
|
||||
|
||||
|
||||
<!-- Individual beilage page entry -->
|
||||
<div
|
||||
@@ -125,7 +156,13 @@
|
||||
data-beilage="true">
|
||||
<div class="flex items-center justify-between gap-2 mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="icon-container">{{ if $firstItem }}{{ PageIcon $firstItem.PageIcon }}{{ else }}{{ PageIcon $iconType }}{{ end }}</span>
|
||||
<span class="icon-container"
|
||||
>{{ if $firstItem }}
|
||||
{{ PageIcon $firstItem.PageIcon }}
|
||||
{{ else }}
|
||||
{{ PageIcon $iconType }}
|
||||
{{ end }}</span
|
||||
>
|
||||
<a
|
||||
href="#beilage-1-page-{{ $page }}"
|
||||
class="page-number-inhalts font-bold text-slate-700 bg-amber-50 px-2 py-1 rounded text-sm transition-colors duration-200 hover:bg-amber-100 no-underline"
|
||||
@@ -147,50 +184,68 @@
|
||||
<div class="space-y-0">
|
||||
{{ if $pageItems }}
|
||||
{{ range $individualPiece := $pageItems }}
|
||||
<div
|
||||
class="inhalts-entry py-1 px-0 bg-slate-50 rounded hover:bg-slate-100 transition-colors duration-200 {{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
continuation-entry
|
||||
{{ end }}"
|
||||
data-page="{{ $page }}"
|
||||
{{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
data-is-continuation="true"
|
||||
{{ end }}
|
||||
style="{{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
display: none;
|
||||
{{ end }}">
|
||||
{{ template "_inhaltsverzeichnis_eintrag" $individualPiece.PieceByIssue }}
|
||||
<div
|
||||
class="inhalts-entry py-1 px-0 bg-slate-50 rounded hover:bg-slate-100 transition-colors duration-200 {{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
continuation-entry
|
||||
{{ end }}"
|
||||
data-page="{{ $page }}"
|
||||
{{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
data-is-continuation="true"
|
||||
{{ end }}
|
||||
style="{{ if $individualPiece.PieceByIssue.IsContinuation }}
|
||||
display: none;
|
||||
{{ end }}">
|
||||
{{ template "_inhaltsverzeichnis_eintrag" $individualPiece.PieceByIssue }}
|
||||
|
||||
|
||||
<!-- Links zu anderen Teilen: -->
|
||||
{{ if and (not $individualPiece.PieceByIssue.IsContinuation) (gt (len $individualPiece.IssueRefs) 1) }}
|
||||
<div class="mt-1 pt-1 border-t border-slate-100">
|
||||
<div class="flex items-center flex-wrap gap-1">
|
||||
<i class="ri-links-line text-slate-600 text-sm mr-1"></i>
|
||||
{{ range $issue := $individualPiece.IssueRefs }}
|
||||
<a
|
||||
href="/{{- $issue.When -}}/{{- $issue.Nr -}}{{- if $issue.Von -}}
|
||||
{{- if $issue.Beilage -}}
|
||||
#beilage-{{ $issue.Beilage }}-page-{{ $issue.Von }}
|
||||
{{- else -}}
|
||||
#page-{{ $issue.Von }}
|
||||
{{- end -}}
|
||||
{{- end -}}"
|
||||
class="inline-flex items-center gap-1 px-2 py-1 bg-slate-100 text-slate-700 rounded-md text-xs font-medium hover:bg-slate-200 transition-colors duration-150"
|
||||
{{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year
|
||||
$model.Datum.When.Year)
|
||||
-}}
|
||||
aria-current="page"
|
||||
{{ end }}>
|
||||
<i class="ri-calendar-line text-xs"></i>
|
||||
{{- $issue.When.Year }} Nr.
|
||||
{{ $issue.Nr -}}
|
||||
</a>
|
||||
{{ end }}
|
||||
<!-- Links zu anderen Teilen: -->
|
||||
{{ if and (not $individualPiece.PieceByIssue.IsContinuation) (gt (len $individualPiece.IssueRefs) 1) }}
|
||||
<div class="mt-1 pt-1 border-t border-slate-100">
|
||||
<div class="flex items-center flex-wrap gap-1">
|
||||
<i class="ri-links-line text-slate-600 text-sm mr-1"></i>
|
||||
{{ range $index, $issue := $individualPiece.IssueRefs }}
|
||||
<div
|
||||
class="inline-flex items-center gap-1 px-2 py-1 bg-slate-100 rounded-md text-xs font-medium hover:bg-slate-200 transition-colors duration-150 {{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year $model.Datum.When.Year) -}}
|
||||
bg-red-100 text-red-700
|
||||
{{ end }}">
|
||||
<span class="text-black text-xs font-bold">{{ add $index 1 }}</span>
|
||||
<span class="w-px h-3 bg-slate-300 mx-1"></span>
|
||||
<a
|
||||
href="/{{- $issue.When -}}/{{- $issue.Nr -}}{{- if $issue.Von -}}
|
||||
{{- if $issue.Beilage -}}
|
||||
#beilage-{{ $issue.Beilage }}-page-{{ $issue.Von }}
|
||||
{{- else -}}
|
||||
#page-{{ $issue.Von }}
|
||||
{{- end -}}
|
||||
{{- end -}}"
|
||||
class="text-slate-700 no-underline hover:text-slate-900 {{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year $model.Datum.When.Year) -}}
|
||||
text-red-700 pointer-events-none
|
||||
{{ end }}"
|
||||
{{- if and (eq $issue.Nr $model.Number.No) (eq $issue.When.Year
|
||||
$model.Datum.When.Year)
|
||||
-}}
|
||||
aria-current="page"
|
||||
{{ end }}>
|
||||
{{- $issueKey := printf "%d-%d" $issue.When.Year $issue.Nr -}}
|
||||
{{- $issueData := GetIssue $issueKey -}}
|
||||
{{- if $issueData -}}
|
||||
{{ $issueData.Datum.When.Day }}.{{ $issueData.Datum.When.Month }}.{{ $issueData.Datum.When.Year }}
|
||||
{{- else -}}
|
||||
{{ $issue.When.Year }} Nr.
|
||||
{{ $issue.Nr }}
|
||||
{{- end -}}
|
||||
{{- if $issue.Von }}
|
||||
S.
|
||||
{{ $issue.Von }}
|
||||
{{- end -}}
|
||||
</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<!-- Empty page indicator -->
|
||||
<div class="inhalts-entry py-1 px-0">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
0
views/routes/kategorie/head.gohtml
Normal file
0
views/routes/kategorie/head.gohtml
Normal file
0
views/routes/kontakt/body.gohtml
Normal file
0
views/routes/kontakt/body.gohtml
Normal file
0
views/routes/ort/head.gohtml
Normal file
0
views/routes/ort/head.gohtml
Normal file
0
views/routes/search/head.gohtml
Normal file
0
views/routes/search/head.gohtml
Normal file
@@ -77,17 +77,958 @@ function setup_templates() {
|
||||
});
|
||||
}
|
||||
|
||||
// INFO: This is intended to be callled once on website load
|
||||
// ===========================
|
||||
// NEWSPAPER LAYOUT FUNCTIONS
|
||||
// ===========================
|
||||
|
||||
// Global variables for state management
|
||||
window.highlightObserver = window.highlightObserver || null;
|
||||
window.currentPageContainers = window.currentPageContainers || [];
|
||||
window.currentActiveIndex = window.currentActiveIndex || 0;
|
||||
window.pageObserver = window.pageObserver || null;
|
||||
window.scrollTimeout = window.scrollTimeout || null;
|
||||
|
||||
// Page highlighting functionality
|
||||
function initializePageHighlighting() {
|
||||
// Clean up existing observer
|
||||
if (window.highlightObserver) {
|
||||
window.highlightObserver.disconnect();
|
||||
window.highlightObserver = null;
|
||||
}
|
||||
|
||||
// Get all page containers
|
||||
const pageContainers = document.querySelectorAll(".newspaper-page-container");
|
||||
|
||||
// Set up intersection observer for active page tracking
|
||||
window.highlightObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
checkAndHighlightVisiblePages();
|
||||
},
|
||||
{
|
||||
rootMargin: "-20% 0px -70% 0px",
|
||||
},
|
||||
);
|
||||
|
||||
// Observe all page containers
|
||||
pageContainers.forEach((container) => {
|
||||
window.highlightObserver.observe(container);
|
||||
});
|
||||
}
|
||||
|
||||
function checkAndHighlightVisiblePages() {
|
||||
const visiblePageNumbers = [];
|
||||
const allPageContainers = document.querySelectorAll(".newspaper-page-container");
|
||||
|
||||
// Find visible page numbers
|
||||
allPageContainers.forEach((container) => {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
const visibleTop = Math.max(containerRect.top, 0);
|
||||
const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
|
||||
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
||||
const containerHeight = containerRect.height;
|
||||
|
||||
const visibilityRatio = visibleHeight / containerHeight;
|
||||
const isVisible = visibilityRatio >= 0.5;
|
||||
|
||||
const pageImg = container.querySelector("img[data-page]");
|
||||
const pageNumber = pageImg ? pageImg.getAttribute("data-page") : "unknown";
|
||||
|
||||
if (isVisible && pageImg && pageNumber && !visiblePageNumbers.includes(pageNumber)) {
|
||||
visiblePageNumbers.push(pageNumber);
|
||||
}
|
||||
});
|
||||
|
||||
// Show continuations only for visible pages
|
||||
showContinuationsForVisiblePages(visiblePageNumbers);
|
||||
|
||||
// Highlight visible pages
|
||||
if (visiblePageNumbers.length > 0) {
|
||||
markCurrentPagesInInhaltsverzeichnis(visiblePageNumbers);
|
||||
}
|
||||
}
|
||||
|
||||
function showContinuationsForVisiblePages(visiblePageNumbers) {
|
||||
// Hide all continuation entries by default
|
||||
document.querySelectorAll(".continuation-entry").forEach((entry) => {
|
||||
entry.style.display = "none";
|
||||
});
|
||||
|
||||
// Show continuation entries only for visible pages
|
||||
visiblePageNumbers.forEach((pageNumber) => {
|
||||
const pageEntry = document.querySelector(`[data-page-container="${pageNumber}"]`);
|
||||
if (pageEntry) {
|
||||
const continuationEntries = pageEntry.querySelectorAll(".continuation-entry");
|
||||
continuationEntries.forEach((entry) => {
|
||||
entry.style.display = "";
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update work titles based on highlighted state
|
||||
updateWorkTitles(visiblePageNumbers);
|
||||
|
||||
// Update page entry visibility after showing/hiding continuations
|
||||
updatePageEntryVisibility();
|
||||
}
|
||||
|
||||
function updateWorkTitles(visiblePageNumbers) {
|
||||
// Reset all work titles to short form
|
||||
document.querySelectorAll(".work-title").forEach((titleElement) => {
|
||||
const shortTitle = titleElement.getAttribute("data-short-title");
|
||||
if (shortTitle) {
|
||||
titleElement.textContent = shortTitle;
|
||||
}
|
||||
});
|
||||
|
||||
// Update work titles to full form for highlighted pages
|
||||
visiblePageNumbers.forEach((pageNumber) => {
|
||||
const pageEntry = document.querySelector(`[data-page-container="${pageNumber}"]`);
|
||||
if (pageEntry) {
|
||||
const workTitles = pageEntry.querySelectorAll(".work-title");
|
||||
workTitles.forEach((titleElement) => {
|
||||
const fullTitle = titleElement.getAttribute("data-full-title");
|
||||
if (fullTitle && fullTitle !== titleElement.getAttribute("data-short-title")) {
|
||||
titleElement.textContent = fullTitle;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePageEntryVisibility() {
|
||||
// Check each page entry to see if it has any visible content
|
||||
document.querySelectorAll(".page-entry").forEach((pageEntry) => {
|
||||
const allEntries = pageEntry.querySelectorAll(".inhalts-entry");
|
||||
let hasVisibleContent = false;
|
||||
|
||||
// Check if any entry is visible
|
||||
allEntries.forEach((entry) => {
|
||||
const computedStyle = window.getComputedStyle(entry);
|
||||
if (computedStyle.display !== "none") {
|
||||
hasVisibleContent = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Hide the entire page entry if it has no visible content
|
||||
if (hasVisibleContent) {
|
||||
pageEntry.style.display = "";
|
||||
} else {
|
||||
pageEntry.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function markCurrentPageInInhaltsverzeichnis(pageNumber) {
|
||||
markCurrentPagesInInhaltsverzeichnis([pageNumber]);
|
||||
}
|
||||
|
||||
function markCurrentPagesInInhaltsverzeichnis(pageNumbers) {
|
||||
console.log("markCurrentPagesInInhaltsverzeichnis called with:", pageNumbers);
|
||||
|
||||
// Reset all page container borders to default
|
||||
document.querySelectorAll("[data-page-container]").forEach((container) => {
|
||||
if (container.hasAttribute("data-beilage")) {
|
||||
container.classList.remove("border-red-500");
|
||||
container.classList.add("border-amber-400");
|
||||
} else {
|
||||
container.classList.remove("border-red-500");
|
||||
container.classList.add("border-slate-300");
|
||||
}
|
||||
});
|
||||
|
||||
// Reset all page numbers in Inhaltsverzeichnis
|
||||
document.querySelectorAll(".page-number-inhalts").forEach((pageNum) => {
|
||||
pageNum.classList.remove("text-red-600", "font-bold");
|
||||
pageNum.classList.add("text-slate-700", "font-semibold");
|
||||
pageNum.style.textDecoration = "";
|
||||
pageNum.style.pointerEvents = "";
|
||||
// Restore hover effects
|
||||
if (pageNum.classList.contains("bg-blue-50")) {
|
||||
pageNum.classList.add("hover:bg-blue-100");
|
||||
} else if (pageNum.classList.contains("bg-amber-50")) {
|
||||
pageNum.classList.add("hover:bg-amber-100");
|
||||
}
|
||||
// Keep original background colors
|
||||
if (!pageNum.classList.contains("bg-amber-50") && !pageNum.classList.contains("bg-blue-50")) {
|
||||
pageNum.classList.add("bg-blue-50");
|
||||
}
|
||||
});
|
||||
|
||||
// Reset all containers and links in Inhaltsverzeichnis
|
||||
document.querySelectorAll(".inhalts-entry").forEach((container) => {
|
||||
container.classList.add("hover:bg-slate-100");
|
||||
container.style.cursor = "";
|
||||
});
|
||||
|
||||
document.querySelectorAll('.inhalts-entry a[href*="/"]').forEach((link) => {
|
||||
link.classList.remove("no-underline");
|
||||
if (link.classList.contains("bg-blue-50")) {
|
||||
link.classList.add("hover:bg-blue-100");
|
||||
}
|
||||
});
|
||||
|
||||
// Find and highlight the current page numbers
|
||||
const highlightedElements = [];
|
||||
|
||||
pageNumbers.forEach((pageNumber) => {
|
||||
// Find the exact page entry for this page number
|
||||
const pageNumElement = document.querySelector(
|
||||
`.page-number-inhalts[data-page-number="${pageNumber}"]`,
|
||||
);
|
||||
|
||||
if (pageNumElement) {
|
||||
// Highlight the page number
|
||||
pageNumElement.classList.remove(
|
||||
"text-slate-700",
|
||||
"hover:bg-blue-100",
|
||||
"hover:bg-amber-100",
|
||||
);
|
||||
pageNumElement.classList.add("text-red-600", "font-bold");
|
||||
pageNumElement.style.textDecoration = "none";
|
||||
pageNumElement.style.pointerEvents = "none";
|
||||
highlightedElements.push(pageNumElement);
|
||||
|
||||
// Highlight the page container's left border
|
||||
const pageContainer = document.querySelector(`[data-page-container="${pageNumber}"]`);
|
||||
if (pageContainer) {
|
||||
pageContainer.classList.remove("border-slate-300", "border-amber-400");
|
||||
pageContainer.classList.add("border-red-500");
|
||||
}
|
||||
|
||||
// Make links in the current page non-clickable and remove hover effects
|
||||
const parentEntry = pageNumElement.closest(".page-entry");
|
||||
if (parentEntry) {
|
||||
// Remove hover effects from the container
|
||||
const entryContainers = parentEntry.querySelectorAll(".inhalts-entry");
|
||||
entryContainers.forEach((container) => {
|
||||
container.classList.remove("hover:bg-slate-100");
|
||||
container.style.cursor = "default";
|
||||
});
|
||||
|
||||
// Also handle issue reference links
|
||||
parentEntry.querySelectorAll('a[href*="/"]').forEach((link) => {
|
||||
if (link.getAttribute("aria-current") === "page") {
|
||||
link.style.textDecoration = "none";
|
||||
link.style.pointerEvents = "none";
|
||||
link.classList.add("no-underline");
|
||||
link.classList.remove("hover:bg-blue-100");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-scroll to first highlighted element if it exists
|
||||
if (highlightedElements.length > 0) {
|
||||
scrollToHighlightedPage(highlightedElements[0]);
|
||||
}
|
||||
|
||||
// Also highlight page indicators
|
||||
document.querySelectorAll(".page-indicator").forEach((indicator) => {
|
||||
indicator.classList.remove("text-red-600", "font-bold");
|
||||
indicator.classList.add("text-slate-600", "font-semibold");
|
||||
// Keep original backgrounds
|
||||
if (!indicator.classList.contains("bg-amber-50")) {
|
||||
indicator.classList.add("bg-blue-50");
|
||||
}
|
||||
});
|
||||
|
||||
// Highlight page indicators for all current pages
|
||||
pageNumbers.forEach((pageNumber) => {
|
||||
const pageIndicator = document.querySelector(`.page-indicator[data-page="${pageNumber}"]`);
|
||||
if (pageIndicator) {
|
||||
pageIndicator.classList.remove("text-slate-600");
|
||||
pageIndicator.classList.add("text-red-600", "font-bold");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scrollToHighlightedPage(element) {
|
||||
// Check if the element is in a scrollable container
|
||||
const inhaltsContainer = element.closest(".lg\\:overflow-y-auto");
|
||||
if (inhaltsContainer) {
|
||||
// Calculate position
|
||||
const containerRect = inhaltsContainer.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
// Check if element is not fully visible
|
||||
const isAboveContainer = elementRect.top < containerRect.top;
|
||||
const isBelowContainer = elementRect.bottom > containerRect.bottom;
|
||||
|
||||
if (isAboveContainer || isBelowContainer) {
|
||||
// Scroll to make element visible with some padding
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modal functions
|
||||
function enlargePage(imgElement, pageNumber, isFromSpread) {
|
||||
const modal = document.getElementById("pageModal");
|
||||
const modalImage = document.getElementById("modalImage");
|
||||
|
||||
modalImage.src = imgElement.src;
|
||||
modalImage.alt = imgElement.alt;
|
||||
|
||||
modal.classList.remove("hidden");
|
||||
|
||||
// Mark current page when enlarged
|
||||
markCurrentPageInInhaltsverzeichnis(pageNumber);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById("pageModal");
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Page navigation functions
|
||||
function initializePageTracking() {
|
||||
// Clean up existing observer
|
||||
if (window.pageObserver) {
|
||||
window.pageObserver.disconnect();
|
||||
window.pageObserver = null;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
window.currentPageContainers = Array.from(
|
||||
document.querySelectorAll(".newspaper-page-container"),
|
||||
);
|
||||
window.currentActiveIndex = 0;
|
||||
updateButtonStates();
|
||||
|
||||
// Set up new observer
|
||||
const existingObserver = document.querySelector(".newspaper-page-container");
|
||||
if (existingObserver) {
|
||||
let visibleContainers = new Set();
|
||||
|
||||
window.pageObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const containerIndex = window.currentPageContainers.indexOf(entry.target);
|
||||
if (containerIndex !== -1) {
|
||||
if (entry.isIntersecting) {
|
||||
visibleContainers.add(containerIndex);
|
||||
} else {
|
||||
visibleContainers.delete(containerIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update currentActiveIndex to the first (topmost) visible container
|
||||
if (visibleContainers.size > 0) {
|
||||
const sortedVisible = Array.from(visibleContainers).sort((a, b) => a - b);
|
||||
const newActiveIndex = sortedVisible[0];
|
||||
if (newActiveIndex !== window.currentActiveIndex) {
|
||||
window.currentActiveIndex = newActiveIndex;
|
||||
updateButtonStates();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: "-20% 0px -70% 0px",
|
||||
},
|
||||
);
|
||||
|
||||
window.currentPageContainers.forEach((container) => {
|
||||
window.pageObserver.observe(container);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToPreviousPage() {
|
||||
if (window.currentActiveIndex > 0) {
|
||||
// Find the first page that's not currently visible
|
||||
let targetIndex = -1;
|
||||
|
||||
// Check which pages are currently visible
|
||||
const visibleIndexes = [];
|
||||
window.currentPageContainers.forEach((container, index) => {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
const visibleTop = Math.max(containerRect.top, 0);
|
||||
const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
|
||||
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
||||
const containerHeight = containerRect.height;
|
||||
|
||||
const visibilityRatio = visibleHeight / containerHeight;
|
||||
if (visibilityRatio >= 0.3) { // Consider visible if 30% or more is showing
|
||||
visibleIndexes.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
// Find the first non-visible page before the current visible range
|
||||
const minVisibleIndex = Math.min(...visibleIndexes);
|
||||
for (let i = minVisibleIndex - 1; i >= 0; i--) {
|
||||
if (!visibleIndexes.includes(i)) {
|
||||
targetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no non-visible page found, go to the page just before the visible range
|
||||
if (targetIndex === -1 && minVisibleIndex > 0) {
|
||||
targetIndex = minVisibleIndex - 1;
|
||||
}
|
||||
|
||||
if (targetIndex >= 0) {
|
||||
window.currentActiveIndex = targetIndex;
|
||||
window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
|
||||
// Update button states after a brief delay to let intersection observer catch up
|
||||
setTimeout(() => {
|
||||
updateButtonStates();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToNextPage() {
|
||||
if (window.currentActiveIndex < window.currentPageContainers.length - 1) {
|
||||
// Find the first page that's not currently visible
|
||||
let targetIndex = -1;
|
||||
|
||||
// Check which pages are currently visible
|
||||
const visibleIndexes = [];
|
||||
window.currentPageContainers.forEach((container, index) => {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
const visibleTop = Math.max(containerRect.top, 0);
|
||||
const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
|
||||
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
||||
const containerHeight = containerRect.height;
|
||||
|
||||
const visibilityRatio = visibleHeight / containerHeight;
|
||||
if (visibilityRatio >= 0.3) { // Consider visible if 30% or more is showing
|
||||
visibleIndexes.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
// Find the first non-visible page after the current visible range
|
||||
const maxVisibleIndex = Math.max(...visibleIndexes);
|
||||
for (let i = maxVisibleIndex + 1; i < window.currentPageContainers.length; i++) {
|
||||
if (!visibleIndexes.includes(i)) {
|
||||
targetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no non-visible page found, go to the page just after the visible range
|
||||
if (targetIndex === -1 && maxVisibleIndex < window.currentPageContainers.length - 1) {
|
||||
targetIndex = maxVisibleIndex + 1;
|
||||
}
|
||||
|
||||
if (targetIndex >= 0 && targetIndex < window.currentPageContainers.length) {
|
||||
window.currentActiveIndex = targetIndex;
|
||||
window.currentPageContainers[window.currentActiveIndex].scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
|
||||
// Update button states after a brief delay to let intersection observer catch up
|
||||
setTimeout(() => {
|
||||
updateButtonStates();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBeilage() {
|
||||
// Check if we're currently viewing a Beilage section
|
||||
const isViewingBeilage = isCurrentlyInBeilageSection();
|
||||
|
||||
if (isViewingBeilage) {
|
||||
// Go back to main issue (first main page)
|
||||
const firstMainPage = document.querySelector('#newspaper-content .newspaper-page-container');
|
||||
if (firstMainPage) {
|
||||
firstMainPage.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Go to first beilage container
|
||||
const beilageContainer = document.querySelector('[class*="border-t-2 border-amber-200"]');
|
||||
if (beilageContainer) {
|
||||
beilageContainer.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isCurrentlyInBeilageSection() {
|
||||
// Check which pages are currently visible
|
||||
const visibleIndexes = [];
|
||||
window.currentPageContainers.forEach((container, index) => {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
const visibleTop = Math.max(containerRect.top, 0);
|
||||
const visibleBottom = Math.min(containerRect.bottom, viewportHeight);
|
||||
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
||||
const containerHeight = containerRect.height;
|
||||
|
||||
const visibilityRatio = visibleHeight / containerHeight;
|
||||
if (visibilityRatio >= 0.3) { // Consider visible if 30% or more is showing
|
||||
visibleIndexes.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
// Check if any visible page is a Beilage page
|
||||
for (const index of visibleIndexes) {
|
||||
const container = window.currentPageContainers[index];
|
||||
if (container && container.id && container.id.includes('beilage-')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function updateButtonStates() {
|
||||
const prevBtn = document.getElementById("prevPageBtn");
|
||||
const nextBtn = document.getElementById("nextPageBtn");
|
||||
const beilageBtn = document.getElementById("beilageBtn");
|
||||
|
||||
if (prevBtn) {
|
||||
if (window.currentActiveIndex <= 0) {
|
||||
prevBtn.style.display = "none";
|
||||
} else {
|
||||
prevBtn.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
if (nextBtn) {
|
||||
if (window.currentActiveIndex >= window.currentPageContainers.length - 1) {
|
||||
nextBtn.style.display = "none";
|
||||
} else {
|
||||
nextBtn.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
// Update Beilage button based on current location
|
||||
if (beilageBtn) {
|
||||
const isViewingBeilage = isCurrentlyInBeilageSection();
|
||||
const icon = beilageBtn.querySelector('i');
|
||||
|
||||
if (isViewingBeilage) {
|
||||
// Show "Go to Main Issue" state - use gray styling
|
||||
beilageBtn.title = "Zur Hauptausgabe";
|
||||
beilageBtn.className = "w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 hover:text-gray-800 border border-gray-300 transition-colors duration-200 flex items-center justify-center cursor-pointer";
|
||||
if (icon) {
|
||||
icon.className = "ri-file-text-line text-lg lg:text-xl";
|
||||
}
|
||||
} else {
|
||||
// Show "Go to Beilage" state - use amber styling
|
||||
beilageBtn.title = "Zu Beilage";
|
||||
beilageBtn.className = "w-14 h-10 lg:w-16 lg:h-12 px-2 py-1 bg-amber-100 hover:bg-amber-200 text-amber-700 hover:text-amber-800 border border-amber-300 transition-colors duration-200 flex items-center justify-center cursor-pointer";
|
||||
if (icon) {
|
||||
icon.className = "ri-attachment-line text-lg lg:text-xl";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Share and citation functions
|
||||
function shareCurrentPage() {
|
||||
const button = document.getElementById("shareLinkBtn");
|
||||
|
||||
// Get current page information
|
||||
let pageInfo = "";
|
||||
|
||||
// Try to get the currently visible page number from active containers
|
||||
if (
|
||||
window.currentActiveIndex !== undefined &&
|
||||
window.currentPageContainers &&
|
||||
window.currentPageContainers[window.currentActiveIndex]
|
||||
) {
|
||||
const activeContainer = window.currentPageContainers[window.currentActiveIndex];
|
||||
const pageElement = activeContainer.querySelector("[data-page]");
|
||||
if (pageElement) {
|
||||
const pageNumber = pageElement.getAttribute("data-page");
|
||||
pageInfo = `#page-${pageNumber}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the shareable URL
|
||||
const currentUrl = window.location.origin + window.location.pathname + pageInfo;
|
||||
|
||||
// Try to use Web Share API if available (mobile browsers)
|
||||
if (navigator.share) {
|
||||
navigator
|
||||
.share({
|
||||
title: document.title,
|
||||
url: currentUrl,
|
||||
})
|
||||
.catch((err) => {
|
||||
// Fallback to clipboard
|
||||
copyToClipboard(currentUrl, button);
|
||||
});
|
||||
} else {
|
||||
// Fallback: copy to clipboard
|
||||
copyToClipboard(currentUrl, button);
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(text, button) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
showSimplePopup(button, "Link kopiert!");
|
||||
})
|
||||
.catch((err) => {
|
||||
showSimplePopup(button, "Kopieren fehlgeschlagen");
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
const successful = document.execCommand("copy");
|
||||
showSimplePopup(button, successful ? "Link kopiert!" : "Kopieren fehlgeschlagen");
|
||||
} catch (err) {
|
||||
showSimplePopup(button, "Kopieren fehlgeschlagen");
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateCitation() {
|
||||
const button = document.getElementById("citationBtn");
|
||||
|
||||
// Get current page and issue information
|
||||
const issueInfo = document.title || "KGPZ";
|
||||
const currentUrl = window.location.href;
|
||||
|
||||
// Basic citation format (can be expanded later)
|
||||
const currentDate = new Date().toLocaleDateString("de-DE");
|
||||
const citation = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${issueInfo}. Digital verfügbar unter: ${currentUrl} (Zugriff: ${currentDate}).`;
|
||||
|
||||
// Copy to clipboard
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.writeText(citation)
|
||||
.then(() => {
|
||||
showSimplePopup(button, "Zitation kopiert!");
|
||||
})
|
||||
.catch((err) => {
|
||||
showSimplePopup(button, "Kopieren fehlgeschlagen");
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = citation;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
const successful = document.execCommand("copy");
|
||||
showSimplePopup(button, successful ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
|
||||
} catch (err) {
|
||||
showSimplePopup(button, "Kopieren fehlgeschlagen");
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showSimplePopup(button, message) {
|
||||
// Remove any existing popup
|
||||
const existingPopup = document.querySelector(".simple-popup");
|
||||
if (existingPopup) {
|
||||
existingPopup.remove();
|
||||
}
|
||||
|
||||
// Create popup element
|
||||
const popup = document.createElement("div");
|
||||
popup.className = "simple-popup";
|
||||
popup.textContent = message;
|
||||
|
||||
// Style the popup
|
||||
popup.style.cssText = `
|
||||
position: fixed;
|
||||
background: #374151;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
// Position popup next to button
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const viewportWidth = window.innerWidth;
|
||||
|
||||
// Calculate position relative to viewport (since we're using fixed positioning)
|
||||
let left = buttonRect.left - 10;
|
||||
let top = buttonRect.bottom + 8;
|
||||
|
||||
// Ensure popup doesn't go off-screen
|
||||
const popupWidth = 120; // Estimated popup width
|
||||
const popupHeight = 32; // Estimated popup height
|
||||
|
||||
// Adjust horizontal position if too far right
|
||||
if (left + popupWidth > viewportWidth) {
|
||||
left = buttonRect.right - popupWidth + 10;
|
||||
}
|
||||
|
||||
// Adjust vertical position if too far down (show above button instead)
|
||||
if (top + popupHeight > viewportHeight) {
|
||||
top = buttonRect.top - popupHeight - 8;
|
||||
}
|
||||
|
||||
popup.style.left = Math.max(5, left) + "px";
|
||||
popup.style.top = Math.max(5, top) + "px";
|
||||
|
||||
// Add to page
|
||||
document.body.appendChild(popup);
|
||||
|
||||
// Fade in
|
||||
setTimeout(() => {
|
||||
popup.style.opacity = "1";
|
||||
}, 10);
|
||||
|
||||
// Auto-remove after 2 seconds
|
||||
setTimeout(() => {
|
||||
popup.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
if (popup.parentNode) {
|
||||
popup.remove();
|
||||
}
|
||||
}, 200);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Hash navigation functions
|
||||
function scrollToPageFromHash() {
|
||||
const hash = window.location.hash;
|
||||
let pageNumber = "";
|
||||
let targetContainer = null;
|
||||
|
||||
if (hash.startsWith("#page-")) {
|
||||
pageNumber = hash.replace("#page-", "");
|
||||
|
||||
// Try to find exact page container first
|
||||
targetContainer = document.getElementById(`page-${pageNumber}`);
|
||||
|
||||
// If not found, try to find container that contains this page
|
||||
if (!targetContainer) {
|
||||
// Look for double-spread containers that contain this page
|
||||
const containers = document.querySelectorAll(".newspaper-page-container[data-pages]");
|
||||
for (const container of containers) {
|
||||
const pages = container.getAttribute("data-pages");
|
||||
if (pages && pages.split(",").includes(pageNumber)) {
|
||||
targetContainer = container;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still not found, try beilage containers
|
||||
if (!targetContainer) {
|
||||
targetContainer =
|
||||
document.getElementById(`beilage-1-page-${pageNumber}`) ||
|
||||
document.getElementById(`beilage-2-page-${pageNumber}`) ||
|
||||
document.querySelector(`[id*="beilage"][id*="page-${pageNumber}"]`);
|
||||
}
|
||||
} else if (hash.startsWith("#beilage-")) {
|
||||
// Handle beilage-specific hashes like #beilage-1-page-101
|
||||
const match = hash.match(/#beilage-(\d+)-page-(\d+)/);
|
||||
if (match) {
|
||||
const beilageNum = match[1];
|
||||
pageNumber = match[2];
|
||||
targetContainer = document.getElementById(`beilage-${beilageNum}-page-${pageNumber}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetContainer && pageNumber) {
|
||||
setTimeout(() => {
|
||||
targetContainer.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
|
||||
// Highlight the specific page in the table of contents
|
||||
markCurrentPageInInhaltsverzeichnis(pageNumber);
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Page-specific utilities
|
||||
function copyPagePermalink(pageNumber, button, isBeilage = false) {
|
||||
let pageFragment = "";
|
||||
if (isBeilage) {
|
||||
pageFragment = `#beilage-1-page-${pageNumber}`;
|
||||
} else {
|
||||
pageFragment = `#page-${pageNumber}`;
|
||||
}
|
||||
|
||||
const currentUrl = window.location.origin + window.location.pathname + pageFragment;
|
||||
|
||||
// Copy to clipboard
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.writeText(currentUrl)
|
||||
.then(() => {
|
||||
showSimplePopup(button, "Link kopiert!");
|
||||
})
|
||||
.catch((err) => {
|
||||
showSimplePopup(button, "Kopieren fehlgeschlagen");
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = currentUrl;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
const successful = document.execCommand("copy");
|
||||
showSimplePopup(button, successful ? "Link kopiert!" : "Kopieren fehlgeschlagen");
|
||||
} catch (err) {
|
||||
showSimplePopup(button, "Kopieren fehlgeschlagen");
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generatePageCitation(pageNumber, button) {
|
||||
// Get current issue information
|
||||
const issueInfo = document.title || "KGPZ";
|
||||
const currentUrl = `${window.location.origin}${window.location.pathname}#page-${pageNumber}`;
|
||||
|
||||
// Basic citation format for specific page
|
||||
const currentDate = new Date().toLocaleDateString("de-DE");
|
||||
const citation = `Königsberger Gelehrte und Politische Zeitung (KGPZ). ${issueInfo}, Seite ${pageNumber}. Digital verfügbar unter: ${currentUrl} (Zugriff: ${currentDate}).`;
|
||||
|
||||
// Copy to clipboard
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.writeText(citation)
|
||||
.then(() => {
|
||||
showSimplePopup(button, "Zitation kopiert!");
|
||||
})
|
||||
.catch((err) => {
|
||||
showSimplePopup(button, "Kopieren fehlgeschlagen");
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = citation;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
const successful = document.execCommand("copy");
|
||||
showSimplePopup(button, successful ? "Zitation kopiert!" : "Kopieren fehlgeschlagen");
|
||||
} catch (err) {
|
||||
showSimplePopup(button, "Kopieren fehlgeschlagen");
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize newspaper layout functionality
|
||||
function initializeNewspaperLayout() {
|
||||
// Initialize page highlighting
|
||||
initializePageHighlighting();
|
||||
|
||||
// Initialize page tracking
|
||||
initializePageTracking();
|
||||
|
||||
// Set up scroll handler
|
||||
window.addEventListener("scroll", function () {
|
||||
clearTimeout(window.scrollTimeout);
|
||||
window.scrollTimeout = setTimeout(() => {
|
||||
checkAndHighlightVisiblePages();
|
||||
updateButtonStates(); // Update button states including Beilage toggle
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// Initialize hash handling
|
||||
scrollToPageFromHash();
|
||||
window.addEventListener("hashchange", scrollToPageFromHash);
|
||||
|
||||
// Set up keyboard shortcuts
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Export functions for global access
|
||||
window.enlargePage = enlargePage;
|
||||
window.closeModal = closeModal;
|
||||
window.scrollToPreviousPage = scrollToPreviousPage;
|
||||
window.scrollToNextPage = scrollToNextPage;
|
||||
window.scrollToBeilage = scrollToBeilage;
|
||||
window.shareCurrentPage = shareCurrentPage;
|
||||
window.generateCitation = generateCitation;
|
||||
window.copyPagePermalink = copyPagePermalink;
|
||||
window.generatePageCitation = generatePageCitation;
|
||||
|
||||
// INFO: This is intended to be called once on website load
|
||||
function setup() {
|
||||
setup_xslt();
|
||||
setup_templates();
|
||||
|
||||
// Initialize newspaper layout if present
|
||||
if (document.querySelector(".newspaper-page-container")) {
|
||||
initializeNewspaperLayout();
|
||||
}
|
||||
|
||||
// Set up HTMX event handlers
|
||||
htmx.on("htmx:load", function (_) {
|
||||
// INFO: We can instead use afterSettle; and also clear the map with
|
||||
// xslt_processors.clear();
|
||||
setup_xslt();
|
||||
});
|
||||
|
||||
setup_templates();
|
||||
// HTMX event handling for newspaper layout
|
||||
document.body.addEventListener("htmx:afterSwap", function (event) {
|
||||
setTimeout(() => {
|
||||
if (document.querySelector(".newspaper-page-container")) {
|
||||
initializeNewspaperLayout();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
document.body.addEventListener("htmx:afterSettle", function (event) {
|
||||
setTimeout(() => {
|
||||
if (document.querySelector(".newspaper-page-container")) {
|
||||
initializeNewspaperLayout();
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
document.body.addEventListener("htmx:load", function (event) {
|
||||
setTimeout(() => {
|
||||
if (document.querySelector(".newspaper-page-container")) {
|
||||
initializeNewspaperLayout();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
export { setup };
|
||||
|
||||
@@ -92,4 +92,72 @@ body::before {
|
||||
main {
|
||||
@apply grow shrink-0;
|
||||
}
|
||||
|
||||
/* =============================
|
||||
NEWSPAPER LAYOUT STYLES
|
||||
============================= */
|
||||
|
||||
/* Essential styles that cannot be represented in Tailwind */
|
||||
.newspaper-page-container {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Page header backdrop blur */
|
||||
.newspaper-page-container .absolute .page-indicator {
|
||||
pointer-events: auto;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* Responsive header scaling */
|
||||
@media (max-width: 768px) {
|
||||
.newspaper-page-container .absolute {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
/* Image size constraints with Tailwind utilities */
|
||||
.single-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.single-page .newspaper-page-image {
|
||||
max-width: min(400px, 100%);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Larger screens */
|
||||
@media (min-width: 1280px) {
|
||||
.single-page .newspaper-page-image {
|
||||
max-width: min(600px, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Very wide screens */
|
||||
@media (min-width: 1536px) {
|
||||
.single-page .newspaper-page-image {
|
||||
max-width: min(700px, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile constraints */
|
||||
@media (max-width: 640px) {
|
||||
.single-page .newspaper-page-image {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Navigation button hover styles */
|
||||
button#prevPageBtn:hover:not([style*="display: none"]),
|
||||
button#nextPageBtn:hover,
|
||||
button#beilageBtn:hover {
|
||||
background-color: rgb(209 213 219) !important; /* gray-300 */
|
||||
color: rgb(55 65 81) !important; /* gray-700 */
|
||||
}
|
||||
|
||||
button#beilageBtn:hover {
|
||||
background-color: rgb(254 215 170) !important; /* amber-200 */
|
||||
color: rgb(146 64 14) !important; /* amber-800 */
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user