背景

Mirador は IIIF 対応の画像ビューアで、複数の IIIF マニフェストを並べて比較閲覧できる。複数機関が公開するマニフェストを一画面に並べて表示する際、各ウィンドウのタイトルにはマニフェストの label がそのまま表示される。

しかし、自プロジェクト独自の名称をウィンドウタイトルとして表示したいケースがある。例えば、マニフェストの label が個別の冊次情報を含む長い文字列であるのに対し、資料群を示す短い名称で表示したい場合などである。

制約:マニフェストの中身は変えてはいけない

他機関が公開している IIIF マニフェストを読み込んで表示する以上、その中身を改変して表示することは避けたい。fetch のインターセプトや Mirador 内部状態の書き換えでマニフェスト JSON の label を差し替える方法もあるが、これは実質的にマニフェストの改変にあたる。

変更すべきは Mirador が画面上に描画したウィンドウのタイトル表示(DOM)だけ であり、マニフェストのデータ自体はオリジナルのまま保持したい。

試したアプローチと結果

1. Mirador.actions.receiveManifest による内部状態の書き換え

// Mirador の store を監視し、マニフェスト読み込み後に label を書き換え
store.subscribe(function () {
  var state = store.getState();
  if (manifests[manifestId] && manifests[manifestId].json && !overridden[manifestId]) {
    overridden[manifestId] = true;
    var updatedJson = JSON.parse(JSON.stringify(manifests[manifestId].json));
    updatedJson.label = customTitle;
    store.dispatch(Mirador.actions.receiveManifest(manifestId, updatedJson));
  }
});

結果:動作しない。 unpkg から配信される Mirador 4 の UMD ビルドでは Mirador.actionsundefined であり、この API は利用できなかった。

2. window.fetch のインターセプト

var originalFetch = window.fetch;
window.fetch = function (url, options) {
  return originalFetch.call(this, url, options).then(function (response) {
    if (titleMap[url]) {
      return response.text().then(function (text) {
        var json = JSON.parse(text);
        json.label = titleMap[url];
        return new Response(JSON.stringify(json), { /* ... */ });
      });
    }
    return response;
  });
};

結果:動作するが、マニフェストの中身を改変していることになる。 Mirador が受け取る JSON 自体が書き換わるため、情報パネル等でも改変後の label が表示される。他機関のマニフェストを改変して表示することへの懸念から不採用。

3. DOM のテキストノード全体を走査して差し替え

var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
var node;
while ((node = walker.nextNode())) {
  if (node.nodeValue.trim() === originalLabel) {
    node.nodeValue = customTitle;
  }
}

結果:影響範囲が広すぎる。 ウィンドウタイトルだけでなく、マニフェスト情報パネルのメタデータ表示など、同じ label テキストが出現するあらゆる箇所が書き換わってしまう。

4. ウィンドウ上部バーの h2 要素だけを対象にする(採用)

Mirador 4 のウィンドウ上部バーは以下の DOM 構造を持つ:

<nav class="...WindowTopBar-root" aria-label="ウィンドウ操作">
  <div class="...mirador-window-top-bar...">
    <h2 class="MuiTypography-root MuiTypography-h2 MuiTypography-noWrap ...">
      マニフェストのlabelがここに表示される
    </h2>
    <!-- ボタン類 -->
  </div>
</nav>

この h2 要素だけを対象に差し替えれば、マニフェストの中身もメタデータ表示も一切変更せず、ウィンドウのタイトル表示だけを制御できる。

最終的な実装

URLパラメータに titles を追加し、セミコロン区切りで各マニフェストに対応するカスタムタイトルを指定する。

mirador/index.html?manifest=<URL1>;<URL2>&titles=<タイトル1>;<タイトル2>

タイトルマップの構築

var titleMap = {};
if (vars["titles"] && vars["manifest"]) {
  var titleArray = vars["titles"].split(";");
  for (var i = 0; i < manifestArray.length && i < titleArray.length; i++) {
    var titleValue = decodeURIComponent(titleArray[i]);
    if (titleValue) {
      titleMap[decodeURIComponent(manifestArray[i])] = titleValue;
    }
  }
}

manifest パラメータと同じ順序・同じ数で titles を指定する。マニフェスト URL をキー、カスタムタイトルを値とするマップを構築する。

DOM 差し替え処理

if (Object.keys(titleMap).length > 0) {
  var store = miradorInstance.store;
  var replaceTimer = null;
  var observer = new MutationObserver(scheduleReplace);

  function getReplacements() {
    var state = store.getState();
    var ws = state.windows || {};
    var ms = state.manifests || {};
    var result = [];
    Object.keys(ws).forEach(function (wid) {
      var mid = ws[wid].manifestId;
      if (!titleMap[mid] || !ms[mid] || !ms[mid].json) return;
      var original = ms[mid].json.label;
      if (original && typeof original === "string") {
        result.push({ original: original, custom: titleMap[mid] });
      }
    });
    return result;
  }

  function replaceTitles() {
    var replacements = getReplacements();
    if (replacements.length === 0) return;

    var container = document.getElementById("mirador");
    observer.disconnect();

    replacements.forEach(function (r) {
      container
        .querySelectorAll(".mirador-window-top-bar h2")
        .forEach(function (h2) {
          if (h2.textContent.trim() === r.original) {
            h2.textContent = r.custom;
          }
        });
    });

    observer.observe(container, { childList: true, subtree: true });
  }

  function scheduleReplace() {
    if (replaceTimer) clearTimeout(replaceTimer);
    replaceTimer = setTimeout(replaceTitles, 50);
  }

  store.subscribe(scheduleReplace);
  observer.observe(document.getElementById("mirador"), {
    childList: true,
    subtree: true,
  });
}

ポイントは以下の3点:

  1. store.subscribe — Mirador の Redux store を監視し、マニフェストの読み込み完了を検知する。store からオリジナルの label を取得し、差し替え対象を特定する。
  2. MutationObserver — Mirador が React で再描画するたびに DOM が更新されるため、変更を検知して再度タイトルを差し替える。
  3. observer.disconnect() / observer.observe() — 自身の DOM 変更で MutationObserver が再発火して無限ループになることを防ぐため、差し替え処理中はオブザーバーを一時停止する。

まとめ

観点結果
マニフェスト JSON変更なし(オリジナルのまま)
Mirador 内部状態変更なし
情報パネルのメタデータ変更なし(元の label が表示される)
ウィンドウ上部バーのタイトルカスタムタイトルに差し替え

外部機関が公開する IIIF マニフェストを尊重しつつ、自プロジェクトの文脈に合わせた表示名をユーザーに提示できる。Mirador のプラグイン機構を使わず、標準の DOM API だけで実現できるシンプルな手法である。