1
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
Posted at

はじめに

日本のビジネス文書では「令和6年12月25日」のような和暦表記が広く使われています。紙の申請書や契約書をプリザンターに取り込む際、和暦で記載された日付を西暦の日付項目に手作業で変換するのは地味に手間がかかります。

この記事では、日付項目に和暦入力モーダルを追加して、元号・年・月・日を選択するだけで西暦の日付を自動セットする仕組みを拡張機能だけで実装します。標準の flatpickr(カレンダー入力)と和暦入力を併存させ、ユーザーがどちらの方法でも日付を入力できるようにします。

対応する機能は以下のとおりです。

  • 日付項目ラベル(field-label)の直後に和暦入力ボタンを追加し、クリックで和暦入力モーダルを表示
  • 元号ドロップダウン + 年・月・日の入力欄で和暦日付を指定
  • モーダル内でリアルタイムに西暦変換のプレビューを表示
  • OK ボタンで日付項目に西暦変換した値をセット
  • 5つの元号(明治・大正・昭和・平成・令和)に対応
  • 元号の範囲を超えた和暦年の自動変換(例: 昭和100年 → 令和7年)
  • DateADateZ だけでなく StartTime(開始)・CompletionTime(完了)など全ての日付項目に対応
  • 有効化モードを 3 パターンから選択可能

和暦の元号と西暦の対応

まずは元号と西暦の対応関係を整理しておきます。

元号 略称 開始日 終了日
明治 M 1868年1月25日 1912年7月29日
大正 T 1912年7月30日 1926年12月24日
昭和 S 1926年12月25日 1989年1月7日
平成 H 1989年1月8日 2019年4月30日
令和 R 2019年5月1日 現在

元号の「元年」は西暦換算で 1 年目にあたります。たとえば「令和元年」は 2019 年です。変換式は次のとおりです。

西暦年 = 元号の開始西暦年 + 和暦年 - 1

構成の全体像

今回の実装は拡張スクリプト・拡張スタイルの 2 種類を組み合わせます。

ファイル 配置先 役割
WarekiDateInput.css ExtendedStyles/ モーダルと入力部品のスタイル
WarekiDateInput.js ExtendedScripts/ 和暦入力ボタンの追加、モーダルの制御、変換処理

有効化モード

和暦入力を有効にする日付項目の範囲を、スクリプト先頭の WAREKI_MODE 変数で切り替えられます。

モード WAREKI_MODE の値 動作
全日付項目 'all' エディタ上のすべての日付項目に和暦入力を追加
CSS 指定のみ 'css' フィールド CSS に wareki を設定した項目だけに追加
CSS 除外 'no-css' フィールド CSS に no-wareki を設定した項目以外に追加

対象となる日付項目

プリザンターの日付項目は DateADateZ の 26 項目だけではありません。期限付きテーブル(Issues)では StartTime(開始)と CompletionTime(完了)も日付項目として表示されます。

本スクリプトでは <date-field> カスタム要素をセレクタとして使用しているため、DateADateZ に加えて StartTimeCompletionTime などエディタ上のすべての日付項目が対象になります。

モードの使い分け

  • 'all': 「とりあえず全部の日付項目に和暦入力を付けたい」場合。フィールド CSS の設定は不要です
  • 'css': 「特定の項目だけに和暦入力を付けたい」場合。対象項目のフィールド CSS に wareki を設定します
  • 'no-css': 「基本は全部に付けたいが、一部の項目だけ除外したい」場合。除外したい項目のフィールド CSS に no-wareki を設定します

元号の範囲を超えた和暦年の自動変換

紙の書類には「昭和100年」のように、すでに終了した元号の年数が使われている場合があります。本スクリプトでは、元号の範囲を超えた和暦年を入力しても西暦の日付として正しく計算し、モーダルのプレビューに正しい元号での和暦表記も併記します。

たとえば「昭和100年1月1日」と入力した場合:

  • 西暦: 1926 + 100 - 1 = 2025年 1月1日
  • 正しい和暦: 令和7年 1月1日

プレビューには → 2025年1月1日(水)= 令和7年1月1日 と表示されるため、元号をまたいだ変換でも正しい日付を確認してからセットできます。

変換ロジックの実装

元号の定義

元号情報を配列で定義します。開始日を保持することで、西暦→和暦の逆変換にも対応できるようにしています。

var ERAS = [
    { name: '令和', abbr: 'R', start: [2019, 5, 1] },
    { name: '平成', abbr: 'H', start: [1989, 1, 8] },
    { name: '昭和', abbr: 'S', start: [1926, 12, 25] },
    { name: '大正', abbr: 'T', start: [1912, 7, 30] },
    { name: '明治', abbr: 'M', start: [1868, 1, 25] }
];

配列の並び順は新しい元号が先です。西暦→和暦の変換で先頭からマッチさせるため、この順序にしています。

和暦→西暦変換

元号名(または略称)と和暦年・月・日から Date オブジェクトを生成します。

function warekiToDate(eraName, eraYear, month, day) {
    var era = null;
    for (var i = 0; i < ERAS.length; i++) {
        if (ERAS[i].name === eraName || ERAS[i].abbr === eraName) {
            era = ERAS[i];
            break;
        }
    }
    if (!era) return null;

    var y = era.start[0] + eraYear - 1;
    if (month < 1 || month > 12 || day < 1 || day > 31) return null;

    var d = new Date(y, month - 1, day);
    if (d.getFullYear() !== y || d.getMonth() !== month - 1 || d.getDate() !== day) {
        return null;
    }
    return d;
}

Date コンストラクタの自動補正(例: 2月30日→3月2日)を検出して null を返すことで、存在しない日付の入力を防いでいます。

西暦→和暦変換

逆方向の変換です。Date オブジェクトから元号名・和暦年を算出し、和暦文字列を返します。

function dateToWareki(date) {
    if (!(date instanceof Date) || isNaN(date.getTime())) return null;

    var y = date.getFullYear();
    for (var i = 0; i < ERAS.length; i++) {
        var era = ERAS[i];
        var eraStart = new Date(era.start[0], era.start[1] - 1, era.start[2]);
        if (date >= eraStart) {
            var eraYear = y - era.start[0] + 1;
            var yearStr = eraYear === 1 ? '' : String(eraYear);
            return era.name + yearStr + ''
                + (date.getMonth() + 1) + ''
                + date.getDate() + '';
        }
    }
    return null;
}

日付文字列→元号情報の逆引き

モーダルを開いたときに、日付項目の現在値から元号・和暦年・月・日を逆算してプリセットします。

function dateToEraComponents(date) {
    if (!(date instanceof Date) || isNaN(date.getTime())) return null;

    var y = date.getFullYear();
    for (var i = 0; i < ERAS.length; i++) {
        var era = ERAS[i];
        var eraStart = new Date(era.start[0], era.start[1] - 1, era.start[2]);
        if (date >= eraStart) {
            return {
                eraName: era.name,
                eraYear: y - era.start[0] + 1,
                month: date.getMonth() + 1,
                day: date.getDate()
            };
        }
    }
    return null;
}

和暦入力モーダルの実装(拡張スタイル + 拡張スクリプト)

ボタンとモーダルのスタイル(拡張スタイル)

和暦入力モーダルと入力部品の見た目を定義します。和暦ボタン本体は date-field の Shadow DOM 内に差し込むため、スクリプト側で専用スタイルを注入します。拡張スタイルとして App_Data/Parameters/ExtendedStyles/ に配置します。

ExtendedStyles/WarekiDateInput.css
/* モーダルオーバーレイ */
#wareki-modal {
  display: none;
}

#wareki-modal .wareki-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 9998;
}

/* モーダルダイアログ */
#wareki-modal .wareki-dialog {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
  z-index: 9999;
  width: 360px;
  max-width: 90vw;
}

#wareki-modal .wareki-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 1px solid #e0e0e0;
  font-weight: bold;
}

#wareki-modal .wareki-header button {
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0;
  display: flex;
}

#wareki-modal .wareki-body {
  padding: 20px;
}

#wareki-modal .wareki-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

#wareki-modal .wareki-row-date {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 8px;
}

#wareki-modal .wareki-row label {
  min-width: 36px;
  font-size: 14px;
}

#wareki-modal .wareki-row-date label {
  min-width: auto;
}

#wareki-modal .wareki-row select {
  flex: 1;
  padding: 6px 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
}

#wareki-modal .wareki-row input[type="number"] {
  width: 100%;
  min-width: 0;
  padding: 6px 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
}

#wareki-modal .wareki-preview {
  padding: 10px;
  background: #f5f5f5;
  border-radius: 4px;
  text-align: center;
  font-size: 14px;
  margin-bottom: 4px;
  min-height: 20px;
}

#wareki-modal .wareki-preview.wareki-error {
  color: #c62828;
  background: #ffebee;
}

#wareki-modal .wareki-footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 20px;
  border-top: 1px solid #e0e0e0;
}

#wareki-modal .wareki-footer button {
  padding: 8px 20px;
  border: 1px solid #ccc;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

#wareki-modal .wareki-btn-ok {
  background: var(--primaryColor, #1976d2);
  color: #fff;
  border-color: transparent;
}

#wareki-modal .wareki-btn-ok:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

#wareki-modal .wareki-btn-cancel {
  background: #fff;
}

和暦入力ボタンとモーダルの処理(拡張スクリプト)

日付項目への「暦」ボタン追加、モーダルの表示、和暦→西暦変換を実装します。拡張スクリプトとして App_Data/Parameters/ExtendedScripts/ に配置します。

スクリプト先頭のマスター設定セクションで WAREKI_MODE(有効化モード)と ERAS(元号定義)を環境に合わせて変更できます。新元号が制定された場合は ERAS 配列の先頭に追加してください。

ExtendedScripts/WarekiDateInput.js
$(function () {
  // ┌────────────────────────────────────────┐
  // │  マスター設定(環境に合わせて変更)    │
  // └────────────────────────────────────────┘

  // 有効化モード
  //   'all'    : すべての日付項目に和暦入力を追加
  //   'css'    : フィールド CSS に wareki を設定した項目だけ
  //   'no-css' : フィールド CSS に no-wareki を設定した項目以外
  var WAREKI_MODE = 'all';

  // 元号定義(新しい元号が先)
  var ERAS = [
    { name: '令和', abbr: 'R', start: [2019, 5, 1] },
    { name: '平成', abbr: 'H', start: [1989, 1, 8] },
    { name: '昭和', abbr: 'S', start: [1926, 12, 25] },
    { name: '大正', abbr: 'T', start: [1912, 7, 30] },
    { name: '明治', abbr: 'M', start: [1868, 1, 25] }
  ];

  // ========================================
  // 変換関数
  // ========================================

  function warekiToDate(eraName, eraYear, month, day) {
    var era = null;
    for (var i = 0; i < ERAS.length; i++) {
      if (ERAS[i].name === eraName) { era = ERAS[i]; break; }
    }
    if (!era) return null;

    var y = era.start[0] + eraYear - 1;
    if (month < 1 || month > 12 || day < 1 || day > 31) return null;

    var d = new Date(y, month - 1, day);
    if (d.getFullYear() !== y || d.getMonth() !== month - 1
        || d.getDate() !== day) return null;
    return d;
  }

  function dateToWareki(date) {
    if (!(date instanceof Date) || isNaN(date.getTime())) return null;
    var y = date.getFullYear();
    for (var i = 0; i < ERAS.length; i++) {
      var era = ERAS[i];
      var eraStart = new Date(era.start[0], era.start[1] - 1, era.start[2]);
      if (date >= eraStart) {
        var ey = y - era.start[0] + 1;
        return era.name + (ey === 1 ? '' : ey) + ''
          + (date.getMonth() + 1) + '' + date.getDate() + '';
      }
    }
    return null;
  }

  function dateToEraComponents(date) {
    if (!(date instanceof Date) || isNaN(date.getTime())) return null;
    var y = date.getFullYear();
    for (var i = 0; i < ERAS.length; i++) {
      var era = ERAS[i];
      var eraStart = new Date(era.start[0], era.start[1] - 1, era.start[2]);
      if (date >= eraStart) {
        return {
          eraName: era.name,
          eraYear: y - era.start[0] + 1,
          month: date.getMonth() + 1,
          day: date.getDate()
        };
      }
    }
    return null;
  }

  function formatDateStr(d) {
    var y = d.getFullYear();
    var m = ('0' + (d.getMonth() + 1)).slice(-2);
    var day = ('0' + d.getDate()).slice(-2);
    return y + '/' + m + '/' + day;
  }

  var WEEKDAYS = ['', '', '', '', '', '', ''];

  function formatPreview(d) {
    return d.getFullYear() + '' + (d.getMonth() + 1) + ''
      + d.getDate() + '日(' + WEEKDAYS[d.getDay()] + '';
  }

  // ========================================
  // モーダル HTML 生成
  // ========================================

  function ensureModal() {
    if ($('#wareki-modal').length) return;
    var eraOptions = '';
    for (var i = 0; i < ERAS.length; i++) {
      eraOptions += '<option value="' + ERAS[i].name + '">'
        + ERAS[i].name + '</option>';
    }
    $('body').append(
      '<div id="wareki-modal">'
      + '<div class="wareki-overlay"></div>'
      + '<div class="wareki-dialog">'
      +   '<div class="wareki-header">'
      +     '<span>和暦入力</span>'
      +     '<button type="button" id="wareki-close">'
      +       '<span class="material-symbols-outlined">close</span>'
      +     '</button>'
      +   '</div>'
      +   '<div class="wareki-body">'
      +     '<div class="wareki-row">'
      +       '<label>元号</label>'
      +       '<select id="wareki-era">' + eraOptions + '</select>'
      +     '</div>'
      +     '<div class="wareki-row wareki-row-date">'
      +       '<label>年</label>'
      +       '<input type="number" id="wareki-year" min="1" max="99" value="1">'
      +       '<label>月</label>'
      +       '<input type="number" id="wareki-month" min="1" max="12" value="1">'
      +       '<label>日</label>'
      +       '<input type="number" id="wareki-day" min="1" max="31" value="1">'
      +     '</div>'
      +     '<div id="wareki-preview" class="wareki-preview"></div>'
      +   '</div>'
      +   '<div class="wareki-footer">'
      +     '<button type="button" class="wareki-btn-cancel" id="wareki-cancel">'
      +       'キャンセル</button>'
      +     '<button type="button" class="wareki-btn-ok" id="wareki-ok">'
      +       'OK</button>'
      +   '</div>'
      + '</div>'
      + '</div>'
    );
    $('#wareki-close, .wareki-overlay, #wareki-cancel').on('click', closeModal);
    $('#wareki-era, #wareki-year, #wareki-month, #wareki-day')
      .on('change input', updatePreview);
    $('#wareki-ok').on('click', applyWareki);
  }

  // ========================================
  // モーダル制御
  // ========================================

  var currentTargetId = null;

  function openModal(controlId) {
    ensureModal();
    currentTargetId = controlId;

    // 日付項目の現在値を元号コンポーネントに分解してプリセット
    var currentVal = $('#' + controlId).val();
    if (currentVal) {
      var parts = currentVal.split(/[\/\-]/);
      if (parts.length === 3) {
        var d = new Date(
          parseInt(parts[0], 10),
          parseInt(parts[1], 10) - 1,
          parseInt(parts[2], 10)
        );
        var comp = dateToEraComponents(d);
        if (comp) {
          $('#wareki-era').val(comp.eraName);
          $('#wareki-year').val(comp.eraYear);
          $('#wareki-month').val(comp.month);
          $('#wareki-day').val(comp.day);
        }
      }
    }

    updatePreview();
    $('#wareki-modal').show();
  }

  function closeModal() {
    $('#wareki-modal').hide();
    currentTargetId = null;
  }

  function updatePreview() {
    var eraName = $('#wareki-era').val();
    var eraYear = parseInt($('#wareki-year').val(), 10);
    var month = parseInt($('#wareki-month').val(), 10);
    var day = parseInt($('#wareki-day').val(), 10);
    var $preview = $('#wareki-preview');
    var $ok = $('#wareki-ok');

    if (isNaN(eraYear) || isNaN(month) || isNaN(day)) {
      $preview.text('年・月・日を入力してください')
        .addClass('wareki-error');
      $ok.prop('disabled', true);
      return;
    }

    var d = warekiToDate(eraName, eraYear, month, day);
    if (!d) {
      $preview.text('無効な日付です').addClass('wareki-error');
      $ok.prop('disabled', true);
      return;
    }

    // 正しい元号での和暦表記を併記(元号範囲外の自動変換)
    var text = '' + formatPreview(d);
    var correctWareki = dateToWareki(d);
    if (correctWareki) {
      text += '' + correctWareki;
    }
    $preview.text(text).removeClass('wareki-error');
    $ok.prop('disabled', false);
  }

  function applyWareki() {
    if (!currentTargetId) return;

    var eraName = $('#wareki-era').val();
    var eraYear = parseInt($('#wareki-year').val(), 10);
    var month = parseInt($('#wareki-month').val(), 10);
    var day = parseInt($('#wareki-day').val(), 10);
    var d = warekiToDate(eraName, eraYear, month, day);
    if (!d) return;

    var dateStr = formatDateStr(d);
    var $target = $('#' + currentTargetId);
    $target.val(dateStr).trigger('change');

    closeModal();
  }

  // ========================================
  // 日付項目への和暦ボタン追加
  // ========================================

  function ensureDateFieldButtonStyle(dateFieldEl) {
    if (!dateFieldEl || !dateFieldEl.shadowRoot) return;
    if (dateFieldEl.shadowRoot.querySelector('#wareki-btn-style')) return;

    var style = document.createElement('style');
    style.id = 'wareki-btn-style';
    style.textContent = [
      '.field-date { position: relative; }',
      '.wareki-date-btn {',
      '  position: absolute;',
      '  top: 0;',
      '  right: 24px;',
      '  z-index: 2;',
      '  display: flex;',
      '  align-items: center;',
      '  justify-content: center;',
      '  width: 24px;',
      '  height: 100%;',
      '  margin: 0;',
      '  padding: 0;',
      '  color: var(--base-text);',
      '  background: transparent;',
      '  border: none;',
      '  outline: none;',
      '  cursor: pointer;',
      '}',
      '.wareki-date-btn .material-symbols-sharp {',
      "  font-family: 'Material Symbols outlined';",
      "  font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;",
      '  font-size: 18px;',
      '  line-height: 1;',
      '}',
      '.wareki-date-btn:hover {',
      '  color: var(--primaryColor, #1976d2);',
      '}',
      '.input-date ::slotted(input),',
      '.input-date ::slotted(textarea),',
      '.input-date ::slotted(select) {',
      '  padding-right: 48px !important;',
      '}'
    ].join('\n');

    dateFieldEl.shadowRoot.appendChild(style);
  }

  function findDateFields() {
    var $fields;
    switch (WAREKI_MODE) {
      case 'css':
        $fields = $('.wareki').find('date-field');
        break;
      case 'no-css':
        $fields = $('date-field').filter(function () {
          return !$(this).closest('.no-wareki').length;
        });
        break;
      default: // 'all'
        $fields = $('date-field');
        break;
    }
    return $fields;
  }

  function setupWarekiFields() {
    findDateFields().each(function () {
      var $dateField = $(this);
      var $field = $dateField.closest('[id$="Field"]');
      if (!$field.length) return;

      // 日付コントロールの input を取得
      var $control = $dateField.find('input[type="text"]');
      if (!$control.length) return;

      var controlId = $control.attr('id');
      if (!controlId) return;

      var dateFieldEl = $dateField[0];
      if (!dateFieldEl || !dateFieldEl.shadowRoot) return;

      // 二重初期化を防止
      if (dateFieldEl.shadowRoot.querySelector('.wareki-date-btn')) return;

      ensureDateFieldButtonStyle(dateFieldEl);

      var currentBtn = dateFieldEl.shadowRoot.querySelector('.current-date');
      if (!currentBtn || !currentBtn.parentNode) return;

      var warekiBtn = document.createElement('button');
      warekiBtn.type = 'button';
      warekiBtn.className = 'wareki-date-btn';
      warekiBtn.setAttribute('title', '和暦で入力');
      var iconSpan = document.createElement('span');
      iconSpan.className = 'material-symbols-sharp';
      iconSpan.setAttribute('aria-hidden', 'true');
      iconSpan.textContent = 'swap_horiz';
      warekiBtn.appendChild(iconSpan);
      warekiBtn.addEventListener('click', function (e) {
        e.preventDefault();
        openModal(controlId);
      });

      // current-date ボタンの左側に挿入
      currentBtn.parentNode.insertBefore(warekiBtn, currentBtn);
    });
  }

  // ========================================
  // 初期化と Ajax 再読み込み対応
  // ========================================

  var debounceTimer;
  $(document).ajaxComplete(function () {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(setupWarekiFields, 100);
  });

  setupWarekiFields();
});

処理の流れ

変換アイコンをクリックしてからの処理を図にすると次のようになります。

flatpickr との併存

和暦入力モーダルと標準の flatpickr カレンダーは完全に独立して動作します。ユーザーはどちらの方法でも日付を入力できます。

入力方法 操作 特徴
flatpickr(標準) カレンダーアイコンをクリック→日付を選択 視覚的にカレンダーから選択
和暦入力モーダル 和暦ボタンをクリック→元号・年・月・日を入力 紙の書類から和暦をそのまま入力

和暦入力ボタンは current-date ボタンの左側に配置されるため、入力欄内で視線移動が少なく、ラベル行の崩れも発生しません。

導入手順

  1. WarekiDateInput.cssApp_Data/Parameters/ExtendedStyles/ に配置する
  2. WarekiDateInput.jsApp_Data/Parameters/ExtendedScripts/ に配置する
  3. WarekiDateInput.js の先頭にある WAREKI_MODE を用途に応じて設定する
設定値 フィールド CSS の設定 動作
'all' 不要 すべての日付項目に和暦入力を追加
'css' 対象項目に wareki を設定 指定した項目だけに追加
'no-css' 除外項目に no-wareki を設定 指定した項目以外に追加

拡張スクリプト・拡張スタイルは再起動不要で即時反映されます。

まとめ

  • 日付項目の current-date ボタン左側に和暦入力ボタンを差し込み、入力欄内で自然に使える和暦入力モーダルを実装しました
  • 標準の flatpickr カレンダーと和暦入力モーダルが併存し、ユーザーはどちらの方法でも日付を入力できます
  • 元号の範囲を超えた和暦年(昭和100年など)も正しい西暦日付に変換し、プレビューに正しい元号での和暦表記を併記します
  • DateADateZ だけでなく StartTime(開始)・CompletionTime(完了)などエディタ上のすべての日付項目に対応しています
  • スクリプト先頭のマスター設定(WAREKI_MODEERAS)を変更するだけで、有効化範囲の切り替えや新元号への対応が可能です
1
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
1
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?