【jQuery不使用】JavaScriptとCSSで作るニュースティッカー

2022/07/26
2
サムネイル画像

Webサイトで時々見かける、最新情報を流すテロップのようなアレ。つい最近調べて知ったのですが、ティッカーというそうです。新着情報がパッと目に入るので良いですよね~

これはぜひブログにもつけてみたい…!ということで、簡単に導入できるニュースティッカーを作りました。jQuery不使用で、レスポンシブ表示にも対応しています。

作成したティッカー

今回作ったのはこんなティッカーです。

ニュースが一定時間で上下に遷移して表示されます。文章が長いものは右端まで自動的にスクロールされる仕様です。

ちなみに、今回作成したティッカーはアトラス公式サイトのティッカーから多分にインスパイアを受けて完成に至りました。アトラスさんのサイトがなければ恐らくできてなかったので本当に感謝ですm(_ _)m

ただしこのティッカー、見た目や仕様はそっくりですが単なるパクリではありません。事実、アトラス版ティッカーのjQueryは確認が面倒で1ミリも見ていないので、コードの構成は根本的に違うものになっていると思われます。

今回作成したIB-Note版ティッカーの特徴は以下の通りです。

  • 最小限のJavaScriptとCSSで構成しており、軽量&高速
  • レスポンシブ表示に対応
  • オプションが指定可能
  • コピペで簡単に使える

コードの最適化にこだわっているのはもちろんのこと、使い勝手の良さを考えて作成しました。詳しい内容は後述しますが、上記の通り実用重視の特徴がそろったモデルになっています。

ティッカーのコードと使い方

ソースコード

リストの要素に情報を入力して使います。以下をコピペしてすぐに使うことができます。

HTML

<div class="ticker-wrap">
  <div class="ticker-head">NEWS</div>
  <div class="ticker">
    <ul>
      <li class='ticker-item'><a href="#"><span class="ticker-date"></span><span class="ticker-title"></span></a></li>
      <li class='ticker-item'><a href="#"><span class="ticker-date"></span><span class="ticker-title"></span></a></li>
      <li class='ticker-item'><a href="#"><span class="ticker-date"></span><span class="ticker-title"></span></a></li>
      <li class='ticker-item'><a href="#"><span class="ticker-date"></span><span class="ticker-title"></span></a></li>
      <li class='ticker-item'><a href="#"><span class="ticker-date"></span><span class="ticker-title"></span></a></li>
    </ul>
  </div>
</div>

.ticker-dateのほうに日付を、.ticker-titleのほうにタイトルを入力してください。

JavaScript

<script type="text/javascript">
(function(window, document) {
  const animTime = 5000; // アニメーション時間(ms)
  const speed = 100; // テキストの移動速度(px)
  const limit = 0; // ブレークポイント(px)
  let animId;
  let isRunning = false;

  const ticker = document.querySelector('.ticker');
  loadTicker();

  function loadTicker() {
    tickerAnim();
    animId = setInterval(tickerAnim, animTime);
    isRunning = true;
  }

  // アニメーション処理
  function tickerAnim() {
    const items = ticker.querySelectorAll('.ticker-item');
    const running = ticker.querySelector('.run');
    let idx, link, first, next;
    if (!running) { // 実行中の要素がない場合(初回のみ)
      first = items[0];
      link = first.querySelector('a');
      first.classList.add('fadeInDown', 'run');
      first.style.zIndex = 1;
      setTimeout(textMove, 1000, link); // 第3引数に引数linkを指定。こうしないと即実行されてしまうので注意。
    } else {
      for (let i = 0; i < items.length; i++) {
        if (items[i] == running) {
          idx = i; // 実行中要素のインデックスを取得
          break;
        }
      }
      next = items[(idx + 1) % items.length];
      running.classList.replace('fadeInDown', 'fadeOutDown');
      setTimeout(() => {
        running.classList.remove('fadeOutDown', 'run');
        running.style.zIndex = 0;
        link = running.querySelector('a');
        link.style.transform = 'none';
        next.classList.add('fadeInDown', 'run');
        next.style.zIndex = 1;
        link = next.querySelector('a');
        setTimeout(textMove, 1000, link);
      }, 300);
    }
  }

  // テキスト移動処理
  function textMove(elm) {
    const move = elm.parentNode.clientWidth - elm.clientWidth;
    if (move < 0) {
      elm.style.transform = 'translateX(' + move + 'px)';
      elm.style.transitionDuration = Math.abs(move) / speed + 's';
    }
  }

  // ウィンドウサイズ変更時
  window.addEventListener('resize', () => {
    const windowWidth = window.innerWidth;
    if (windowWidth <= limit) {
      ticker.style.display = 'none';
      clearInterval(animId);
      isRunning = false;
    } else {
      if (!isRunning) {
        ticker.style.display = 'block';
        animId = setInterval(tickerAnim, animTime);
        isRunning = true;
      }
    }
  });
})(window, document);
</script>

animTimeが上下の表示を切り替えるまでの時間(デフォルトは5秒)、speedが流れるテキストの移動速度(デフォルトは100px)です。

limitはブレークポイント(デフォルトは適用なし)で、指定した横幅を基準にティッカーの表示・非表示を切り替えます。例えばlimitを700と設定した場合、ウィンドウ幅が700px以下となったときティッカーが表示されなくなります。

CSS

<style>
.ticker-wrap {
  display: flex;
  width: 100%;
  background: #000;
  padding: 4px;
}
.ticker-head {
  width: calc(4em + 8px);
  font-style: italic;
  color: dodgerblue;
  line-height: 30px;
  padding: 0 4px;
}
.ticker {
  width: 100%;
  height: 30px;
  font-size: 15px;
  background: #fff;
  line-height: 30px;
  padding: 0 6px;
  overflow: hidden;
}
.ticker ul {
  position: relative;
  list-style: none;
  height: 100%;
  padding: 0;
  margin: 0;
}
.ticker-item {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  padding-right: 0;
  opacity: 0;
}
.ticker-item a {
  display: inline-block;
  width: auto;
  color: #333;
  white-space: nowrap;
  text-decoration: none;
  transition: transform 5s linear;
}
.ticker-date {
  font-weight: bold;
}
.ticker-title {
  margin-left: 10px;
}
.ticker-new {
  color: red;
  margin-left: 10px;
  animation: blink 1s ease-in-out infinite alternate;
}
.fadeInDown {
  opacity: 0;
}
.fadeInDown.run {
  animation: fadeInDown 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) forwards;
}
.fadeOutDown {
  opacity: 1;
}
.fadeOutDown.run {
  animation: fadeOutDown 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) forwards;
}
@keyframes fadeInDown {
  0% {
    opacity: 0;
    transform: translateY(-30px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}
@keyframes fadeOutDown {
  0% {
    opacity: 1;
    transform: translateY(0);
  }
  100% {
    opacity: 0;
    transform: translateY(30px);
  }
}
@keyframes blink {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}
</style>

最初にお見せしたサンプルと同じスタイルです。デザインはお好みで調整してください。

新着記事ティッカー(Blogger用)

Bloggerユーザーの方限定の内容ですが、フィードを利用して新着記事の情報を自動的に表示するティッカーを作ったのでよければご利用ください。以下のソースコードをまるっとコピペするだけで使えます。

ソースコード

<div class="ticker-wrap">
  <div class="ticker-head">NEWS</div>
  <div class="ticker"></div>
</div>
<script type="text/javascript">
//<![CDATA[
(function(window, document) {
  const maxnum = 5; // フィード数
  const periodDay = 7; // New!表示日数
  const animTime = 5000; // アニメーション時間(ms)
  const speed = 100; // テキストの移動速度(px)
  const limit = 0; // ブレークポイント(px)
  let animId;
  let isRunning = false;

  const ticker = document.querySelector('.ticker');
  loadTicker();

  function loadTicker() {
    // フィードを取得
    const feedUrl = '/feeds/posts/summary?alt=json&max-results=' + maxnum;
    fetch(feedUrl)
      .then((response) => {
        return response.json();
      })
      .then((json) => {
        // ティッカーHTMLの生成
        let entry, date, title, url;
        let html = '<ul>';
        let current = new Date();
        for (let i = 0; i < json.feed.entry.length; i++) {
          entry = json.feed.entry[i];
          date = entry.published.$t.substr(0, 10).replace(/-/g, ".");
          title = entry.title.$t;
          for (let j = 0; j < entry.link.length; j++) {
            if (entry.link[j].rel == 'alternate') {
              url = entry.link[j].href;
              break;
            }
          }
          html += '<li class="ticker-item"><a href="' + url + '"><span class="ticker-date">' + date + '</span><span class="ticker-title">'+ title + '</span>';
          date = new Date(entry.published.$t);
          date.setDate(date.getDate() + periodDay);
          if (current < date) {
            html += '<span class="ticker-new">New!</span>';
          }
          html += '</a></li>';
        }
        html += '</ul>';
        ticker.innerHTML = html;

        tickerAnim();
        animId = setInterval(tickerAnim, animTime);
        isRunning = true;
      })
      .catch((error) => {
        console.log(error);
      });
  }

  // アニメーション処理
  function tickerAnim() {
    const items = ticker.querySelectorAll('.ticker-item');
    const running = ticker.querySelector('.run');
    let idx, link, first, next;
    if (!running) { // 実行中の要素がない場合(初回のみ)
      first = items[0];
      link = first.querySelector('a');
      first.classList.add('fadeInDown', 'run');
      first.style.zIndex = 1;
      setTimeout(textMove, 1000, link); // 第3引数に引数linkを指定。こうしないと即実行されてしまうので注意。
    } else {
      for (let i = 0; i < items.length; i++) {
        if (items[i] == running) {
          idx = i; // 実行中要素のインデックスを取得
          break;
        }
      }
      next = items[(idx + 1) % items.length];
      running.classList.replace('fadeInDown', 'fadeOutDown');
      setTimeout(() => {
        running.classList.remove('fadeOutDown', 'run');
        running.style.zIndex = 0;
        link = running.querySelector('a');
        link.style.transform = 'none';
        next.classList.add('fadeInDown', 'run');
        next.style.zIndex = 1;
        link = next.querySelector('a');
        setTimeout(textMove, 1000, link);
      }, 300);
    }
  }

  // テキスト移動処理
  function textMove(elm) {
    const move = elm.parentNode.clientWidth - elm.clientWidth;
    if (move < 0) {
      elm.style.transform = 'translateX(' + move + 'px)';
      elm.style.transitionDuration = Math.abs(move) / speed + 's';
    }
  }

  // ウィンドウサイズ変更時
  window.addEventListener('resize', () => {
    const windowWidth = window.innerWidth;
    if (windowWidth <= limit) {
      ticker.style.display = 'none';
      clearInterval(animId);
      isRunning = false;
    } else {
      if (!isRunning) {
        ticker.style.display = 'block';
        animId = setInterval(tickerAnim, animTime);
        isRunning = true;
      }
    }
  });
})(window, document);
//]]>
</script>
<style>
.ticker-wrap {
  display: flex;
  width: 100%;
  background: #000;
  padding: 4px;
  box-sizing: border-box;
}
.ticker-head {
  width: calc(4em + 8px);
  font-style: italic;
  color: dodgerblue;
  line-height: 30px;
  padding: 0 4px;
}
.ticker {
  width: 100%;
  height: 30px;
  font-size: 15px;
  background: #fff;
  line-height: 30px;
  padding: 0 6px;
  overflow: hidden;
}
.ticker ul {
  position: relative;
  list-style: none;
  height: 100%;
  padding: 0;
  margin: 0;
}
.ticker-item {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  padding-right: 0;
  opacity: 0;
}
.ticker-item a {
  display: inline-block;
  width: auto;
  color: #333;
  white-space: nowrap;
  text-decoration: none;
  transition: transform 5s linear;
}
.ticker-date {
  font-weight: bold;
}
.ticker-title {
  margin-left: 10px;
}
.ticker-new {
  color: red;
  margin-left: 10px;
  animation: blink 1s ease-in-out infinite alternate;
}
.fadeInDown {
  opacity: 0;
}
.fadeInDown.run {
  animation: fadeInDown 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) forwards;
}
.fadeOutDown {
  opacity: 1;
}
.fadeOutDown.run {
  animation: fadeOutDown 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) forwards;
}
@keyframes fadeInDown {
  0% {
    opacity: 0;
    transform: translateY(-30px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
  }
}
@keyframes fadeOutDown {
  0% {
    opacity: 1;
    transform: translateY(0);
  }
  100% {
    opacity: 0;
    transform: translateY(30px);
  }
}
@keyframes blink {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}
</style>

maxnumが読み込む最大フィード数、periodDayが「New!」マークを付ける期間です。デフォルトでは7日以内に投稿された記事にマークが付きます。付けたくない場合はマイナス値を指定してください。

ティッカーのコードに関する補足

CSSの指定はデフォルトでレスポンシブ表示になっていることに加えて、JavaScriptのほうも動的に要素の幅を取得する仕様になっているため、ウィンドウサイズの変更に影響を受けることなく順調に動作します。

ブレークポイントを設定した場合も、ティッカーが非表示になっているときはスクリプトの実行を中断するため、処理がバックグラウンドで続いてリソースを消費するという心配はありません。

あとがき

JavaScriptで一から作るのは想像より大変でしたが、意地でもjQueryは使いたくない…という思いからなんとか完成させることができました。jQueryを使わない上下遷移型のティッカーは調べた範囲では見つからなかったので、もしかしたらこれが第一号かも?なんて期待しています。

今後もJavaScriptでいろいろ面白いものを作っていきたいと思います。

2件のコメント
こちらのニュースティッカーガジェットを F-light デモサイトに導入させて頂きました。(テーマへの実装ではなくデモサイト独自導入)

当初テキストガジェットをお知らせ用に運用しようかと思ったのですが、ふとこちらのガジェットのことを思い出して試しに置いてみたところ、予想以上にいい感じにハマって大変満足しております(^^)
>ふじやんさん
導入のご報告ありがとうございます!
当ブログのニュースティッカーが活躍しているようで何よりです。
デモサイトのほう拝見しましたが、とてもナチュラルに馴染んでいて安心しました(笑)