0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターのカレンダーに独自の祝日を表示する[第1回:表示編]

0
Posted at

はじめに

プリザンターのカレンダーに、国民の祝日や会社独自の休日を表示したいことがあります。日付を固定したCSSでも実現できますが、毎年CSSを作り直す必要があり、休日の追加や変更にも弱い構成になります。

そこで本連載では、期限付きテーブルを「祝日マスタ」として使用します。第1回は、祝日マスタのレコードを拡張スクリプトから読み込み、次の両方へ表示します。

  • 標準カレンダー
  • FullCalendar

プリザンター本体のソースコードは変更しません。管理画面から登録できる拡張スクリプトと拡張スタイルだけで対応します。

第2回では、Googleカレンダーが公開している日本の祝日カレンダーをバックグラウンドサーバスクリプトで取得し、この祝日マスタへ自動登録します。

構成

カレンダーを表示しているユーザーのログインセッションで祝日マスタを読み込みます。APIキーは不要ですが、ユーザーには祝日マスタサイトの閲覧権限が必要です。プリザンターのアクセス制御を迂回する処理は行いません。

祝日マスタを作成する

期限付きテーブルを1つ作成し、祝日1日につき1レコードを登録します。

項目 用途 設定例
タイトル 表示する祝日名 元日
開始 祝日の日付 2027/1/1

開始項目は「日付」の書式にしておくと運用しやすくなります。サイトを作成したら、URLなどからサイトIDを確認しておきます。

拡張スタイル

次のスタイルには、土曜日、日曜日、祝日の表示をまとめています。

CalendarHolidayMaster.css
/* 標準カレンダー。週の開始曜日が月曜日の場合 */
[id^="CalendarBody"] #Grid tbody tr > td:nth-last-child(2):not(.other-month) {
  background-color: #e8f3ff;
}

[id^="CalendarBody"] #Grid tbody tr > td:last-child:not(.other-month) {
  background-color: #fff0f3;
}

/* FullCalendar */
[id^="FullCalendar"].calendar-container .fc-day-sat:not(.fc-day-other) {
  background-color: #e8f3ff;
}

[id^="FullCalendar"].calendar-container .fc-day-sun:not(.fc-day-other) {
  background-color: #fff0f3;
}

/* 祝日は土日の色より優先する */
.pch-holiday {
  background-color: #ffc0cb !important;
}

.pch-holiday-label {
  color: #b00020;
  font-size: 0.85em;
  font-weight: 600;
  margin-left: 0.5em;
  white-space: nowrap;
}

/* 標準カレンダーの週表示 */
[id^="CalendarBody"] td.is-weekly.pch-holiday {
  position: relative;
}

[id^="CalendarBody"] td.is-weekly > .pch-holiday-label {
  position: absolute;
  right: 0.4em;
  top: 0.25em;
}

標準カレンダーの土日判定には列位置を使用しています。General.jsonのFirstDayOfWeekを変更している環境では、標準カレンダー側のセレクタを表示順に合わせて調整してください。FullCalendarはライブラリが付与する.fc-day-satと.fc-day-sunを使用するため、列位置には依存しません。

拡張スクリプト

次のスクリプトのHOLIDAY_SITE_IDを、先ほど作成した祝日マスタのサイトIDに変更します。既定項目以外を使う場合はDATE_COLUMNとTITLE_COLUMNも変更してください。

CalendarHolidayMaster.js
(function () {
  'use strict';

  // ===== 環境に合わせて変更する設定 =====
  var HOLIDAY_SITE_ID = 123456;
  var DATE_COLUMN = 'StartTime';
  var TITLE_COLUMN = 'Title';
  var PAGE_SIZE = 200;

  var holidayMap = Object.create(null);
  var loaded = false;
  var loading = false;
  var callbacks = [];
  var renderTimer = null;

  function toDateKey(value) {
    // Dateへ変換せず、タイムゾーンによる日付ずれを避ける
    var match = String(value || '').match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
    if (!match) return null;
    return match[1] + '-' + String(match[2]).padStart(2, '0')
      + '-' + String(match[3]).padStart(2, '0');
  }

  function finishLoad(error, records) {
    loading = false;
    if (!error) {
      holidayMap = Object.create(null);
      records.forEach(function (record) {
        var key = toDateKey(record[DATE_COLUMN]);
        if (!key) return;

        var name = String(record[TITLE_COLUMN] || '休日');
        holidayMap[key] = holidayMap[key]
          ? holidayMap[key] + ' / ' + name
          : name;
      });
      loaded = true;
    } else {
      console.warn('祝日マスタを取得できませんでした。', error);
    }

    var waiting = callbacks.slice();
    callbacks.length = 0;
    waiting.forEach(function (callback) {
      callback(!error);
    });
  }

  function fetchPage(offset, records) {
    $p.apiGet({
      id: HOLIDAY_SITE_ID,
      data: {
        ApiVersion: 1.1,
        Offset: offset,
        PageSize: PAGE_SIZE
      },
      done: function (response) {
        var result = response && response.Response;
        var page = result && Array.isArray(result.Data) ? result.Data : [];
        Array.prototype.push.apply(records, page);

        var totalCount = Number(result && result.TotalCount) || records.length;
        if (page.length > 0 && records.length < totalCount) {
          fetchPage(offset + page.length, records);
        } else {
          finishLoad(null, records);
        }
      },
      fail: function (xhr) {
        finishLoad(xhr || new Error('API request failed'), records);
      }
    });
  }

  function loadHolidays(callback) {
    if (loaded) {
      callback(true);
      return;
    }
    callbacks.push(callback);
    if (loading) return;
    loading = true;
    fetchPage(0, []);
  }

  function appendLabel($target, name) {
    if (!$target.length || $target.children('.pch-holiday-label').length) return;
    $('<span>', {
      'class': 'pch-holiday-label',
      text: name
    }).appendTo($target);
  }

  function decorateStandardCalendar() {
    $('[id^="CalendarBody"] #Grid td[data-id]').each(function () {
      var $cell = $(this);
      var key = toDateKey($cell.attr('data-id'));
      var name = key && holidayMap[key];
      if (!name) return;

      $cell.addClass('pch-holiday').attr('data-holiday-name', name);
      var $day = $cell.find('.day').first();
      appendLabel($day.length ? $day : $cell, name);
    });
  }

  function decorateFullCalendar() {
    $('[id^="FullCalendar"].calendar-container [data-date]').each(function () {
      var $element = $(this);
      var key = toDateKey($element.attr('data-date'));
      var name = key && holidayMap[key];
      if (!name) return;

      $element.addClass('pch-holiday').attr('data-holiday-name', name);

      if ($element.hasClass('fc-daygrid-day')) {
        appendLabel($element.find('.fc-daygrid-day-top').first(), name);
      } else if ($element.hasClass('fc-col-header-cell')) {
        appendLabel($element.find('.fc-col-header-cell-cushion').first(), name);
      } else if ($element.hasClass('fc-list-day')) {
        appendLabel($element.find('.fc-list-day-text').first(), name);
      }
    });
  }

  function decorateCalendars() {
    decorateStandardCalendar();
    decorateFullCalendar();
  }

  function scheduleDecoration() {
    clearTimeout(renderTimer);
    renderTimer = setTimeout(function () {
      loadHolidays(function (success) {
        if (success) decorateCalendars();
      });
    }, 50);
  }

  // 標準カレンダーのAjax更新とFullCalendarの初期描画に対応
  var previousCalendarLoad = $p.events.on_calendar_load;
  $p.events.on_calendar_load = function () {
    if (typeof previousCalendarLoad === 'function') {
      previousCalendarLoad.apply(this, arguments);
    }
    scheduleDecoration();
  };

  // FullCalendar内の前後移動などによるDOM再描画にも対応
  $(function () {
    scheduleDecoration();
    var mainForm = document.getElementById('MainForm');
    if (!mainForm || !window.MutationObserver) return;

    var observer = new MutationObserver(function (mutations) {
      var calendarChanged = mutations.some(function (mutation) {
        var target = mutation.target;
        return target.nodeType === 1
          && $(target).closest(
            '[id^="CalendarBody"], [id^="FullCalendar"]'
          ).length > 0;
      });
      if (calendarChanged) scheduleDecoration();
    });
    observer.observe(mainForm, { childList: true, subtree: true });
  });
})();

スクリプトのポイント

ログインセッションで別サイトを取得する

$p.apiGetへ祝日マスタのサイトIDを渡しています。現在のブラウザセッションでAPIが呼び出されるため、APIキーを拡張スクリプトへ埋め込む必要はありません。

祝日マスタに多数のレコードがある場合に備えて、OffsetとPageSizeで全ページを取得します。取得結果は画面内にキャッシュするため、FullCalendarを前後移動するたびにAPIを呼び出すこともありません。

日付を文字列のまま扱う

APIから返された日付をDateへ変換すると、値やブラウザのタイムゾーンによって前日へずれる可能性があります。今回は時刻計算が不要なので、年月日の部分を文字列として取り出し、YYYY-MM-DDへそろえています。

両方のカレンダーへ適用する

標準カレンダーはdata-id="YYYY/M/D"、FullCalendarはdata-date="YYYY-MM-DD"を持つ要素を探します。どちらも同じholidayMapと.pch-holidayクラスを使うため、祝日判定と見た目を共通化できます。

FullCalendarの前後移動は、プリザンターへの再リクエストを伴わずライブラリ内でDOMが描き直されます。そのため、on_calendar_loadに加えてMutationObserverでも再描画を検出します。

対応する表示

カレンダー 対応する表示
標準カレンダー 月、週
FullCalendar 月、週、日、リスト

標準カレンダーの年表示は、1セルが1日ではなく1か月を表すため対象外です。

まとめ

期限付きテーブルを祝日マスタにすることで、CSSを毎年生成し直さなくても祝日の追加や変更を画面へ反映できるようになりました。

  • プリザンター本体の改修は不要
  • 拡張スクリプトと拡張スタイルだけで実装
  • 標準カレンダーとFullCalendarの両方に対応
  • 会社独自の休日もレコード追加だけで管理可能

第2回では、Googleカレンダーの日本の祝日カレンダーから、この祝日マスタへレコードを自動登録します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?