PageSourceSearch

https://www.rosier.de/wp-content/themes/breakdance-zero/js/rss-feed-reader/rss-feed-reader.js

js rosier.de collected 2026-10-02 18:03:49 UTC 23,320 bytes, 681 lines download raw bytes

1/**
2 * RSS Feed Reader mit Paging - Rosier Design
3 * Vanilla JavaScript - Keine Abhängigkeiten
4 * Einbindung nur über ein Script-Tag
5 * Design basiert auf rosier.de/news/
6 */
7
8(function() {
9    'use strict';
10
11    // CSS als String (kompiliert aus SCSS, scoped auf .rosier-rss-feed-wrapper)
12    const RSS_FEED_STYLES = `
13        @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&family=Source+Sans+3:wght@300;400;500;600;700&display=swap');
14        
15        .rosier-rss-feed-wrapper {
16            font-family: 'Source Sans 3', 'Open Sans', -apple-system, BlinkMacSystemFont, sans-serif;
17            max-width: 1200px;
18            margin: 0 auto;
19            color: #333333;
20        }
21        .rosier-rss-feed-wrapper *, .rosier-rss-feed-wrapper *::before, .rosier-rss-feed-wrapper *::after {
22            box-sizing: border-box;
23        }
24        .rosier-rss-feed-wrapper .rss-loading {
25            text-align: center;
26            padding: 80px 20px;
27            color: #666666;
28        }
29        .rosier-rss-feed-wrapper .rss-loading-spinner {
30            width: 48px;
31            height: 48px;
32            border: 3px solid #e0e0e0;
33            border-top: 3px solid #374d46;
34            border-radius: 50%;
35            margin: 0 auto 24px;
36            animation: rosier-rss-spin 0.8s linear infinite;
37        }
38        @keyframes rosier-rss-spin {
39            0% { transform: rotate(0deg); }
40            100% { transform: rotate(360deg); }
41        }
42        .rosier-rss-feed-wrapper .rss-error {
43            text-align: center;
44            padding: 48px 24px;
45            background: #fef2f2;
46            border: 1px solid #fecaca;
47            border-radius: 4px;
48            color: #991b1b;
49        }
50        .rosier-rss-feed-wrapper .rss-retry-btn {
51            margin-top: 20px;
52            padding: 12px 32px;
53            background: #374d46;
54            color: white;
55            border: none;
56            cursor: pointer;
57            font-family: inherit;
58            font-size: 14px;
59            font-weight: 600;
60            text-transform: uppercase;
61            letter-spacing: 0.5px;
62            transition: background 0.3s ease;
63        }
64        .rosier-rss-feed-wrapper .rss-retry-btn:hover {
65            background: #2a3b35;
66        }
67        .rosier-rss-feed-wrapper .rss-feed-items {
68            display: grid;
69            grid-template-columns: repeat(3, 1fr);
70            gap: 30px;
71        }
72        .rosier-rss-feed-wrapper .rss-item {
73            display: flex;
74            flex-direction: column;
75            background: #ffffff;
76            overflow: hidden;
77            box-shadow: 2px 4px 20px 0px rgba(0, 0, 0, 0.06);
78            transition: box-shadow 0.3s ease, transform 0.3s ease;
79        }
80        .rosier-rss-feed-wrapper .rss-item:hover {
81            transform: translateY(-3px);
82            box-shadow: 2px 6px 24px 0px rgba(0, 0, 0, 0.1);
83        }
84        .rosier-rss-feed-wrapper .rss-item:hover .rss-item-title a {
85            color: #374d46;
86        }
87        .rosier-rss-feed-wrapper .rss-item-image {
88            width: 100%;
89            aspect-ratio: 16 / 9;
90            overflow: hidden;
91            position: relative;
92            background: #f8f8f8;
93        }
94        .rosier-rss-feed-wrapper .rss-item-image img {
95            width: 100%;
96            height: 100%;
97            object-fit: cover;
98        }
99        .rosier-rss-feed-wrapper .rss-item-content {
100            padding: 20px;
101            display: flex;
102            flex-direction: column;
103            flex: 1;
104        }
105        .rosier-rss-feed-wrapper .rss-item-meta {
106            display: flex;
107            flex-wrap: wrap;
108            align-items: center;
109            gap: 10px;
110            margin-bottom: 12px;
111        }
112        .rosier-rss-feed-wrapper .rss-item-category {
113            background: #374d46;
114            color: white;
115            padding: 4px 10px;
116            font-size: 0.75em;
117            font-weight: 600;
118            text-transform: uppercase;
119            letter-spacing: 0.5px;
120        }
121        .rosier-rss-feed-wrapper .rss-item-date {
122            color: #666666;
123            font-size: 0.8125em;
124            font-weight: 400;
125        }
126        .rosier-rss-feed-wrapper .rss-item-author {
127            color: #666666;
128            font-size: 0.8125em;
129        }
130        .rosier-rss-feed-wrapper .rss-item-title {
131            margin: 0 0 12px;
132            font-size: 1.25em;
133            font-weight: 400;
134            line-height: 1.4;
135            font-family: 'Source Sans 3', sans-serif;
136        }
137        .rosier-rss-feed-wrapper .rss-item-title a {
138            color: #333333;
139            text-decoration: none;
140            transition: color 0.3s ease;
141        }
142        .rosier-rss-feed-wrapper .rss-item-description {
143            margin: 0 0 16px;
144            color: #666666;
145            font-size: 0.9375em;
146            line-height: 1.6;
147            flex-grow: 1;
148        }
149        .rosier-rss-feed-wrapper .rss-item-readmore {
150            display: inline-flex;
151            align-items: center;
152            justify-content: center;
153            padding: 12px 24px;
154            background: #374d46;
155            color: white;
156            text-decoration: none;
157            font-size: 14px;
158            font-weight: 600;
159            transition: background 0.3s ease;
160            align-self: flex-start;
161            border: none;
162        }
163        .rosier-rss-feed-wrapper .rss-item-readmore:hover {
164            background: #2a3b35;
165            color: white;
166        }
167        .rosier-rss-feed-wrapper .rss-paging {
168            margin-top: 40px;
169            text-align: center;
170        }
171        .rosier-rss-feed-wrapper .rss-paging-list {
172            display: flex;
173            justify-content: center;
174            align-items: center;
175            gap: 8px;
176            list-style: none;
177            margin: 0;
178            padding: 0;
179            flex-wrap: wrap;
180        }
181        .rosier-rss-feed-wrapper .rss-paging-item {
182            display: flex;
183        }
184        .rosier-rss-feed-wrapper .rss-paging-ellipsis {
185            padding: 12px 8px;
186            color: #666666;
187            font-weight: 500;
188        }
189        .rosier-rss-feed-wrapper .rss-paging-btn {
190            display: flex;
191            align-items: center;
192            justify-content: center;
193            min-width: 44px;
194            height: 44px;
195            padding: 12px 20px;
196            background: transparent;
197            border: 2px solid #374d46;
198            color: #374d46;
199            font-family: inherit;
200            font-size: 14px;
201            font-weight: 600;
202            cursor: pointer;
203            transition: all 0.3s ease;
204        }
205        .rosier-rss-feed-wrapper .rss-paging-btn:hover:not(:disabled):not(.active) {
206            background: #374d46;
207            color: white;
208        }
209        .rosier-rss-feed-wrapper .rss-paging-btn.active {
210            background: #374d46;
211            border-color: #374d46;
212            color: white;
213        }
214        .rosier-rss-feed-wrapper .rss-paging-btn:disabled {
215            opacity: 0.4;
216            cursor: not-allowed;
217        }
218        .rosier-rss-feed-wrapper .rss-paging-prev,
219        .rosier-rss-feed-wrapper .rss-paging-next {
220            min-width: auto;
221            padding: 12px 24px;
222        }
223        .rosier-rss-feed-wrapper .rss-paging-info {
224            text-align: center;
225            margin-top: 16px;
226            font-size: 14px;
227            color: #666666;
228        }
229        @media (max-width: 992px) {
230            .rosier-rss-feed-wrapper .rss-feed-items {
231                grid-template-columns: repeat(2, 1fr);
232                gap: 24px;
233            }
234        }
235        @media (max-width: 600px) {
236            .rosier-rss-feed-wrapper .rss-feed-items {
237                grid-template-columns: 1fr;
238                gap: 24px;
239            }
240            .rosier-rss-feed-wrapper .rss-item-title {
241                font-size: 1.125em;
242            }
243            .rosier-rss-feed-wrapper .rss-item-content {
244                padding: 16px;
245            }
246            .rosier-rss-feed-wrapper .rss-paging-list {
247                gap: 6px;
248            }
249            .rosier-rss-feed-wrapper .rss-paging-btn {
250                min-width: 40px;
251                height: 40px;
252                font-size: 13px;
253                padding: 10px 16px;
254            }
255            .rosier-rss-feed-wrapper .rss-paging-number {
256                display: none;
257            }
258            .rosier-rss-feed-wrapper .rss-paging-number.active,
259            .rosier-rss-feed-wrapper .rss-paging-number:first-of-type,
260            .rosier-rss-feed-wrapper .rss-paging-number:last-of-type {
261                display: flex;
262            }
263        }
264        .rosier-rss-feed-wrapper.rss-feed--list .rss-feed-items {
265            display: flex;
266            flex-direction: column;
267            gap: 24px;
268        }
269        .rosier-rss-feed-wrapper.rss-feed--list .rss-item {
270            flex-direction: row;
271            align-items: flex-start;
272        }
273        .rosier-rss-feed-wrapper.rss-feed--list .rss-item-image {
274            width: 320px;
275            min-width: 320px;
276            aspect-ratio: 16/9;
277        }
278        .rosier-rss-feed-wrapper.rss-feed--list .rss-item-content {
279            padding: 20px;
280        }
281        @media (max-width: 768px) {
282            .rosier-rss-feed-wrapper.rss-feed--list .rss-item {
283                flex-direction: column;
284            }
285            .rosier-rss-feed-wrapper.rss-feed--list .rss-item-image {
286                width: 100%;
287                min-width: 100%;
288            }
289            .rosier-rss-feed-wrapper.rss-feed--list .rss-item-content {
290                padding: 16px;
291            }
292        }
293    `;
294
295    // CSS injizieren (nur einmal)
296    function injectStyles() {
297        if (document.getElementById('rosier-rss-feed-styles')) return;
298        
299        const styleElement = document.createElement('style');
300        styleElement.id = 'rosier-rss-feed-styles';
301        styleElement.textContent = RSS_FEED_STYLES;
302        document.head.appendChild(styleElement);
303    }
304
305    class RSSFeedReader {
306        constructor(options = {}) {
307            this.feedUrl = options.feedUrl || 'https://blog.rosier.de/rss.xml';
308            this.containerSelector = options.containerSelector || '#rss-feed-container';
309            this.itemsPerPage = options.itemsPerPage || 5;
310            this.listView = options.listView || false;
311            this.currentPage = 1;
312            this.feedItems = [];
313            this.totalPages = 0;
314            
315            // CSS injizieren
316            //injectStyles();
317            
318            this.init();
319        }
320
321        async init() {
322            const container = document.querySelector(this.containerSelector);
323            if (!container) {
324                console.error(`Container ${this.containerSelector} nicht gefunden`);
325                return;
326            }
327
328            this.container = container;
329            
330            // Wrapper-Klasse hinzufügen
331            this.container.classList.add('rosier-rss-feed-wrapper');
332            if (this.listView) {
333                this.container.classList.add('rss-feed--list');
334            }
335            
336            this.showLoading();
337            
338            try {
339                await this.fetchFeed();
340                this.render();
341            } catch (error) {
342                this.showError(error.message);
343            }
344        }
345
346        async fetchFeed() {
347            // CORS-Proxy verwenden, falls nötig
348            const proxyUrl = 'https://api.allorigins.win/raw?url=';
349            const url = proxyUrl + encodeURIComponent(this.feedUrl);
350
351            try {
352                const response = await fetch(url);
353                if (!response.ok) {
354                    throw new Error(`HTTP Fehler: ${response.status}`);
355                }
356                
357                const xmlText = await response.text();
358                this.parseFeed(xmlText);
359            } catch (error) {
360                // Fallback: Direkter Aufruf versuchen
361                try {
362                    const response = await fetch(this.feedUrl);
363                    const xmlText = await response.text();
364                    this.parseFeed(xmlText);
365                } catch (fallbackError) {
366                    throw new Error('RSS-Feed konnte nicht geladen werden: ' + error.message);
367                }
368            }
369        }
370
371        parseFeed(xmlText) {
372            const parser = new DOMParser();
373            const xmlDoc = parser.parseFromString(xmlText, 'application/xml');
374
375            // Prüfe auf Parser-Fehler
376            const parseError = xmlDoc.querySelector('parsererror');
377            if (parseError) {
378                throw new Error('RSS-Feed konnte nicht geparst werden');
379            }
380
381            // RSS 2.0 Format
382            const items = xmlDoc.querySelectorAll('item');
383            
384            this.feedItems = Array.from(items).map(item => {
385                return {
386                    title: this.getElementText(item, 'title'),
387                    link: this.getElementText(item, 'link'),
388                    description: this.getElementText(item, 'description'),
389                    pubDate: this.getElementText(item, 'pubDate'),
390                    author: this.getElementText(item, 'author') || this.getElementText(item, 'dc\\:creator'),
391                    category: this.getElementText(item, 'category'),
392                    image: this.extractImage(item)
393                };
394            });
395
396            this.totalPages = Math.ceil(this.feedItems.length / this.itemsPerPage);
397        }
398
399        getElementText(parent, tagName) {
400            const element = parent.querySelector(tagName);
401            return element ? element.textContent.trim() : '';
402        }
403
404        extractImage(item) {
405            // Versuche Bild aus verschiedenen Quellen zu extrahieren
406            const enclosure = item.querySelector('enclosure[type^="image"]');
407            if (enclosure) {
408                return enclosure.getAttribute('url');
409            }
410
411            // Suche im media:content
412            const mediaContent = item.querySelector('content[url]');
413            if (mediaContent) {
414                return mediaContent.getAttribute('url');
415            }
416
417            // Suche Bild in der Description
418            const description = this.getElementText(item, 'description');
419            const imgMatch = description.match(/<img[^>]+src=["']([^"']+)["']/i);
420            if (imgMatch) {
421                return imgMatch[1];
422            }
423
424            return '';
425        }
426
427        formatDate(dateString) {
428            if (!dateString) return '';
429            
430            try {
431                const date = new Date(dateString);
432                return date.toLocaleDateString('de-DE', {
433                    year: 'numeric',
434                    month: 'long',
435                    day: 'numeric'
436                });
437            } catch (e) {
438                return dateString;
439            }
440        }
441
442        stripHtml(html) {
443            const tmp = document.createElement('div');
444            tmp.innerHTML = html;
445            return tmp.textContent || tmp.innerText || '';
446        }
447
448        truncateText(text, maxLength = 150) {
449            const stripped = this.stripHtml(text);
450            if (stripped.length <= maxLength) return stripped;
451            return stripped.substring(0, maxLength).trim() + '...';
452        }
453
454        getCurrentPageItems() {
455            const startIndex = (this.currentPage - 1) * this.itemsPerPage;
456            const endIndex = startIndex + this.itemsPerPage;
457            return this.feedItems.slice(startIndex, endIndex);
458        }
459
460        render() {
461            const items = this.getCurrentPageItems();
462            
463            let html = '<div class="rss-feed">';
464            
465            // Feed Items
466            html += '<div class="rss-feed-items">';
467            items.forEach(item => {
468                html += this.renderItem(item);
469            });
470            html += '</div>';
471
472            // Paging
473            if (this.totalPages > 1) {
474                html += this.renderPaging();
475            }
476
477            html += '</div>';
478
479            this.container.innerHTML = html;
480            this.bindEvents();
481        }
482
483        renderItem(item) {
484            const imageHtml = item.image 
485                ? `<div class="rss-item-image"><img src="${item.image}" alt="${this.escapeHtml(item.title)}" loading="lazy"></div>`
486                : '';
487
488            const categoryHtml = item.category 
489                ? `<span class="rss-item-category">${this.escapeHtml(item.category)}</span>`
490                : '';
491
492            const dateHtml = item.pubDate 
493                ? `<span class="rss-item-date">${this.formatDate(item.pubDate)}</span>`
494                : '';
495
496            //const authorHtml = item.author
497            //    ? `<span class="rss-item-author">von ${this.escapeHtml(item.author)}</span>`
498            //    : '';
499
500            const authorHtml = '';
501
502            return `
503                <article class="rss-item">
504                    ${imageHtml}
505                    <div class="rss-item-content">
506                        <div class="rss-item-meta">
507                            ${categoryHtml}
508                            ${dateHtml}
509                            ${authorHtml}
510                        </div>
511                        <h3 class="rss-item-title">
512                            <a href="${this.escapeHtml(item.link)}" target="_blank" rel="noopener noreferrer">
513                                ${this.escapeHtml(item.title)}
514                            </a>
515                        </h3>
516                        <p class="rss-item-description">${this.truncateText(item.description)}</p>
517                        <a href="${this.escapeHtml(item.link)}" class="rss-item-readmore" target="_blank" rel="noopener noreferrer">
518                            Weiterlesen
519                        </a>
520                    </div>
521                </article>
522            `;
523        }
524
525        renderPaging() {
526            let pagingHtml = '<nav class="rss-paging" aria-label="Seitennavigation">';
527            pagingHtml += '<ul class="rss-paging-list">';
528
529            // Zurück-Button
530            pagingHtml += `
531                <li class="rss-paging-item">
532                    <button class="rss-paging-btn rss-paging-prev" 
533                            data-page="${this.currentPage - 1}" 
534                            ${this.currentPage === 1 ? 'disabled' : ''}>
535                        ← Zurück
536                    </button>
537                </li>
538            `;
539
540            // Seitenzahlen
541            const pageNumbers = this.getPageNumbers();
542            pageNumbers.forEach(pageNum => {
543                if (pageNum === '...') {
544                    pagingHtml += `<li class="rss-paging-item rss-paging-ellipsis">...</li>`;
545                } else {
546                    const isActive = pageNum === this.currentPage;
547                    pagingHtml += `
548                        <li class="rss-paging-item">
549                            <button class="rss-paging-btn rss-paging-number ${isActive ? 'active' : ''}" 
550                                    data-page="${pageNum}"
551                                    ${isActive ? 'aria-current="page"' : ''}>
552                                ${pageNum}
553                            </button>
554                        </li>
555                    `;
556                }
557            });
558
559            // Weiter-Button
560            pagingHtml += `
561                <li class="rss-paging-item">
562                    <button class="rss-paging-btn rss-paging-next" 
563                            data-page="${this.currentPage + 1}"
564                            ${this.currentPage === this.totalPages ? 'disabled' : ''}>
565                        Weiter →
566                    </button>
567                </li>
568            `;
569
570            pagingHtml += '</ul>';
571            pagingHtml += `<div class="rss-paging-info">Seite ${this.currentPage} von ${this.totalPages}</div>`;
572            pagingHtml += '</nav>';
573
574            return pagingHtml;
575        }
576
577        getPageNumbers() {
578            const pages = [];
579            const maxVisible = 5;
580            
581            if (this.totalPages <= maxVisible) {
582                for (let i = 1; i <= this.totalPages; i++) {
583                    pages.push(i);
584                }
585            } else {
586                // Immer erste Seite
587                pages.push(1);
588
589                if (this.currentPage > 3) {
590                    pages.push('...');
591                }
592
593                // Seiten um aktuelle Seite
594                const start = Math.max(2, this.currentPage - 1);
595                const end = Math.min(this.totalPages - 1, this.currentPage + 1);
596
597                for (let i = start; i <= end; i++) {
598                    pages.push(i);
599                }
600
601                if (this.currentPage < this.totalPages - 2) {
602                    pages.push('...');
603                }
604
605                // Immer letzte Seite
606                pages.push(this.totalPages);
607            }
608
609            return pages;
610        }
611
612        bindEvents() {
613            const pagingButtons = this.container.querySelectorAll('.rss-paging-btn[data-page]');
614            pagingButtons.forEach(btn => {
615                btn.addEventListener('click', (e) => {
616                    const page = parseInt(e.target.dataset.page, 10);
617                    if (page >= 1 && page <= this.totalPages) {
618                        this.goToPage(page);
619                    }
620                });
621            });
622        }
623
624        goToPage(page) {
625            this.currentPage = page;
626            this.render();
627            
628            // Scroll zum Anfang des Containers
629            this.container.scrollIntoView({ behavior: 'smooth', block: 'start' });
630        }
631
632        showLoading() {
633            this.container.innerHTML = `
634                <div class="rss-loading">
635                    <div class="rss-loading-spinner"></div>
636                    <p>RSS-Feed wird geladen...</p>
637                </div>
638            `;
639        }
640
641        showError(message) {
642            this.container.innerHTML = `
643                <div class="rss-error">
644                    <p>⚠️ ${this.escapeHtml(message)}</p>
645                    <button class="rss-retry-btn" onclick="location.reload()">Erneut versuchen</button>
646                </div>
647            `;
648        }
649
650        escapeHtml(text) {
651            const div = document.createElement('div');
652            div.textContent = text;
653            return div.innerHTML;
654        }
655    }
656
657    // Auto-Initialisierung wenn DOM bereit
658    function autoInit() {
659        // Automatisch initialisieren wenn Container vorhanden
660        if (document.querySelector('#rss-feed-container')) {
661            new RSSFeedReader({
662                feedUrl: 'https://blog.rosier.de/rss.xml',
663                containerSelector: '#rss-feed-container',
664                itemsPerPage: 6
665            });
666        }
667    }
668
669    // DOM Ready Check
670    if (document.readyState === 'loading') {
671        document.addEventListener('DOMContentLoaded', autoInit);
672    } else {
673        autoInit();
674    }
675
676    // Export für manuelle Initialisierung
677    window.RSSFeedReader = RSSFeedReader;
678
679})();
680
681

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.