<div class="c-ticker">
<p>Der Ticker wird zurzeit geladen.</p>
<a class="js-ticker-data" href="/assets/json/ticker-data.json">Zu den Daten des Tickers</a>
<div class="c-ticker--template">
<article class="c-teaser c-teaser--card">
<div class="c-teaser__body">
<div class="c-teaser__header">
<div class="c-teaser__kicker">
<div class="c-teaser__meta">
<div class="c-meta">
<ul cLass="c-meta__list">
<li class="c-meta__item"><strong></strong></li>
<li class="c-meta__item"></li>
</ul>
</div>
</div>
</div>
<h2 class="c-teaser__title"></h2>
</div>
<div class="c-teaser__media">
<picture class="c-ticker__img">
<img src="https://placeimg.com/1/1/nature" alt="">
</picture>
<figure class="c-ticker__video">
<iframe src="https://placeimg.com/1/1/nature" frameborder="0" allow="autoplay; fullscreen" mozallowfullscreen webkitallowfullscreen allowfullscreen></iframe>
</figure>
</div>
<p class="c-teaser__content"></p>
</div>
</article>
</div>
</div>
<div class="c-ticker">
<p>Der Ticker wird zurzeit geladen.</p>
<a class="js-ticker-data" href="{{ url }}">Zu den Daten des Tickers</a>
<div class="c-ticker--template">
<article class="c-teaser c-teaser--card">
<div class="c-teaser__body">
<div class="c-teaser__header">
<div class="c-teaser__kicker">
<div class="c-teaser__meta">
<div class="c-meta">
<ul cLass="c-meta__list">
<li class="c-meta__item"><strong></strong></li>
<li class="c-meta__item"></li>
</ul>
</div>
</div>
</div>
<h2 class="c-teaser__title"></h2>
</div>
<div class="c-teaser__media">
<picture class="c-ticker__img">
<img src="https://placeimg.com/1/1/nature" alt="">
</picture>
<figure class="c-ticker__video">
<iframe src="https://placeimg.com/1/1/nature" frameborder="0"
allow="autoplay; fullscreen" mozallowfullscreen webkitallowfullscreen
allowfullscreen></iframe>
</figure>
</div>
<p class="c-teaser__content"></p>
</div>
</article>
</div>
</div>
{
"name": "default",
"items": [],
"url": "/assets/json/ticker-data.json"
}
/**
@desc Ticker gets a progressively enhanced fallback. Text is only shown after a while and
only if JS fails for some reason. Otherwise, ticker data is faded in by the animation.
*/
.c-ticker {
@media (prefers-reduced-motion: no-preference) {
animation: 300ms show forwards;
}
&--template {
display: none;
}
.c-teaser {
min-height: 0;
}
&__video {
height: 0;
max-width: 100%;
overflow: hidden;
padding-bottom: 56.25%;
position: relative;
iframe,
object,
embed {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
}
}
// Animations
@keyframes show {
0%,
90% {
opacity: 0;
}
100% {
opacity: 1;
}
}
/**
* @desc Renders a news ticker with data retrieved from an API as JSON. Polls every 30 seconds.
* @author Tibor Legat, HDNET GmbH & Co. KG
* @see HAZ-35
* @since 08.10.2019
*/
import dayjs from 'dayjs';
import 'dayjs/locale/de';
class Ticker {
constructor(element) {
this.interval = 30000; // 30s delay requirement.
this.module = element.parentNode;
this.moduleName = 'c-ticker';
this.template = this.module.querySelector(`.${this.moduleName}--template`);
this.entries = new Set();
this.entryNodes = [];
if (this.template) {
this.url = element.href || document.location.href;
this.templateNode = this.template.cloneNode(true);
this.module.querySelector('a').href = '';
// init module once.
this.getJSON(true).then(() => {
this.module.innerHTML = ''; // remove fallback text.
this.updateContent();
this.initPoll();
});
} else {
new Error(`[Ticker] Elements missing for initialization. Element: ${element}, Template: ${this.template}`);
}
}
static util() {
// Thanks to Andrea Giammarchi // @see Addy Osmani
const
reEscape = /[&<>'"]/g;
const reUnescape = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
const oEscape = {
'&': '&',
'<': '<',
'>': '>',
"'": ''',
'"': '"',
};
const oUnescape = {
'&': '&',
'&': '&',
'<': '<',
'<': '<',
'>': '>',
'>': '>',
''': "'",
''': "'",
'"': '"',
'"': '"',
};
const fnEscape = (m) => oEscape[m];
const fnUnescape = (m) => oUnescape[m];
const { replace } = String.prototype;
return (Object.freeze || Object)({
escape: function escape(s) {
return replace.call(s, reEscape, fnEscape);
},
unescape: function unescape(s) {
return replace.call(s, reUnescape, fnUnescape);
},
});
}
// Tagged template function
static html(pieces, ...args) {
let result = pieces[0];
for (let i = 0; i < args.length; ++i) {
result += Ticker.util().escape(args[i]) + pieces[i + 1];
}
return result;
}
static getLocalizedDate(timestamp) {
return `${dayjs.unix(timestamp)
.format('H:mm')} Uhr`;
}
/**
* @desc Updates content if timestamp is different or adds a new child.
*/
updateContent() {
this.entryNodes.filter(Boolean).forEach((entry) => {
const matchingDomNode = this.module.querySelector(`[data-id="${entry.dataset.id}"]`);
if (matchingDomNode) {
if (matchingDomNode.dataset.time !== entry.dataset.time) {
// replace
matchingDomNode.parentNode.replaceChild(entry, matchingDomNode);
}
} else {
this.module.insertBefore(entry, this.module.firstChild);
}
});
}
// Get JSON data, add items to cleared entry list.
getJSON(once) {
const that = this;
const { url } = once ? that : that;
// const url = once ? that.url : '/assets/json/ticker-data2.json';
return new Promise(((resolve, reject) => {
window.fetch(url)
.then((data) => data.json())
.then((jsonData) => {
this.entries.clear();
// do stuff with the data
jsonData.forEach((entry) => this.entries.add(JSON.stringify(entry)));
that.addItems();
resolve();
}).catch((e) => {
reject(new Error(e));
});
}));
}
/**
*
* @returns {Set<String>}
*/
compareItems() {
const parsedItems = [...this.entries].map((s) => JSON.parse(s));
/**
*
* @type {{uid: String, updated: *}[]}
*/
const itemMap = parsedItems.map((x) => ({ uid: x.uid, updated: x.modified_date }));
const newEntries = new Set();
[...itemMap].forEach((item) => {
const matchingItem = this.module.querySelector(`[data-id="${item.uid}"]`);
if (matchingItem === null || (String(item.updated) !== matchingItem.dataset.time)) {
const target = parsedItems.filter((entry) => entry.uid === item.uid)[0];
newEntries.add(JSON.stringify(target));
}
});
return newEntries;
}
/**
* Creates dom nodes from ``this.entries`` and stacks them in an array.
*/
addItems() {
this.entryNodes = [];
[...this.compareItems()].map((s) => JSON.parse(s)).forEach((entry) => {
const node = this.templateNode.cloneNode(true);
// Media
if (entry.media && entry.media.type) {
const videoWrapper = node.querySelector(`.${this.moduleName}__video`);
const imageWrapper = node.querySelector(`.${this.moduleName}__img`);
if (entry.media.type === 'video') {
node.querySelector('iframe').src = entry.media.iframe || entry.media.uri;
imageWrapper.parentNode.removeChild(imageWrapper);
} else {
node.querySelector('img').src = entry.media.uri;
videoWrapper.parentNode.removeChild(videoWrapper);
}
} else {
const medium = node.querySelector('.c-teaser__media');
medium.parentNode.removeChild(medium);
}
// Meta data
const metaWrapper = node.querySelector('.c-meta__list');
let metaContent = '';
if (entry.author.name) {
metaContent += Ticker.html`<li class="c-meta__item"><strong>${entry.author.name}</strong></li>`;
}
if (entry.publish_date) {
const formattedDate = Ticker.getLocalizedDate(entry.publish_date);
metaContent += Ticker.html`<li class="c-meta__item">${formattedDate}</li>`;
}
metaWrapper.innerHTML = metaContent;
// Text
node.querySelector('.c-teaser__title').innerHTML = entry.title;
node.querySelector('.c-teaser__content').innerHTML = entry.text;
// Enrich content
node.classList.remove(`${this.moduleName}--template`);
node.dataset.time = entry.modified_date;
node.dataset.id = entry.uid;
this.entryNodes[entry.uid] = node;
});
}
/**
* @desc Initializes polling against the given URL to update contents.
* @requires this.interval
*/
initPoll() {
setInterval(() => {
this.getJSON().then(() => {
this.updateContent();
});
}, this.interval);
}
}
export default Ticker;
The ticker
plugin renders datasets from TYPO3 which are supplied via JSON.
In terms of progressive enhancement, there is a link to the JSON to be accessible
for everyone whenever JS is not available.
The fullscreen
feature is only available in the Component preview as the iframe
in which the components are rendered by default doesn’t supply that option. As
such the feature will only show up when you view the Component outside the iframe
in its own preview.