Ticker

100%
<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"
}
  • Content:
    /**
        @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;
        }
    }
    
  • URL: /components/raw/ticker/_ticker.scss
  • Filesystem Path: src/patterns/20-components/ticker/_ticker.scss
  • Size: 852 Bytes
  • Content:
    /**
     * @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 = {
          '&': '&amp;',
          '<': '&lt;',
          '>': '&gt;',
          "'": '&#39;',
          '"': '&quot;',
        };
        const oUnescape = {
          '&amp;': '&',
          '&#38;': '&',
          '&lt;': '<',
          '&#60;': '<',
          '&gt;': '>',
          '&#62;': '>',
          '&apos;': "'",
          '&#39;': "'",
          '&quot;': '"',
          '&#34;': '"',
        };
        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;
    
  • URL: /components/raw/ticker/ticker.js
  • Filesystem Path: src/patterns/20-components/ticker/ticker.js
  • Size: 6.3 KB

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.

Fullscreen

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.