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?

内部リンク先のページを、モーダルで開くJavaScript

0
Last updated at Posted at 2025-08-27

Webサイトで内部ページへのリンクをクリックした際、新しいページに遷移するのではなく、モーダル(ポップアップウィンドウ)で表示する仕組みを作成しました。WordPressのContent Viewsプラグインなど、記事一覧からの詳細表示に特に有効です。

使用例・想定される用途

1. ポートフォリオサイトでの作品詳細表示

<div class="pt-cv-view target-modal" data-modal-width="800" data-modal-height="500">
  <a href="/works/post-001/" class="_self">プロジェクト詳細を見る</a>
</div>

2. ブログ記事の概要からの詳細表示

  • 記事一覧ページで概要を表示
  • 「続きを読む」リンクをモーダルで開く
  • ユーザーは元のページを離れることなく詳細を確認可能

3. 商品カタログでの商品詳細

  • 商品一覧からの詳細情報表示
  • スペック表や詳細画像をモーダルで表示
  • カタログ閲覧の流れを中断しない

仕組みの概要

このシステムは2つのスクリプトで構成されています:

  1. リンク属性変換スクリプト_selfクラスを持つリンクを自動でモーダル対応に変換
  2. モーダル表示スクリプトtarget="_modal"のリンクをモーダル表示

1. リンク属性変換スクリプト

//DOMContentLoaded後に実行
document.addEventListener('DOMContentLoaded', function () {
  //設定オブジェクト
  const config = {
    //対象となるコンテナセレクタ
    containerSelector: '.pt-cv-view.target-modal',
    
    //対象リンクのセレクタパターン(クラス名ベース)
    linkSelector: 'a[href][class*="_self"]',
    
    //デフォルトのモーダルサイズ
    defaultWidth: '800',
    defaultHeight: '500',
    
    //コンテナ別の個別設定(data属性で上書き可能)
    //data-modal-width, data-modal-height でコンテナごとに設定可能
    containerSpecificSettings: true
  };

  //.pt-cv-view.target-modal 内の処理
  const targetModalViews = document.querySelectorAll(config.containerSelector);
  
  targetModalViews.forEach(function(view) {
    //コンテナ固有の設定を取得
    const containerWidth = view.getAttribute('data-modal-width') || config.defaultWidth;
    const containerHeight = view.getAttribute('data-modal-height') || config.defaultHeight;
    
    //対象リンクを検索(クラス名ベース)
    const targetLinks = view.querySelectorAll(config.linkSelector);
    
    targetLinks.forEach(function(link) {
      const href = link.getAttribute('href');
      
      //hrefが存在し、_selfクラスを持つリンクを対象
      if (href && link.classList.contains('_self')) {
        //既にtarget="_modal"が設定されていない場合のみ処理
        if (link.getAttribute('target') !== '_modal') {
          //target="_modal" を追加
          link.setAttribute('target', '_modal');
          
          //モーダルサイズを設定
          link.setAttribute('data-popwidth', containerWidth);
          link.setAttribute('data-popheight', containerHeight);
          
          //デバッグ用ログ
          console.log('Modal attributes added:', {
            url: href,
            width: containerWidth,
            height: containerHeight,
            className: link.className
          });
        }
      }
    });
    
    console.log(`Processed ${view.querySelectorAll('a[target="_modal"]').length} modal links in container`);
  });
});

2. モーダル表示スクリプト

//DOMContentLoaded後に実行
document.addEventListener('DOMContentLoaded', function () {
//_link-openModal.js
const arrModalLinks = Array.prototype.slice.call(document.querySelectorAll('[href][target="_modal"]:not([href*="#"])'));

//モーダル用のスタイルを動的に追加
const modalStyle = document.createElement('style');
modalStyle.textContent = `
/* linkModal:CSS
-------------------------------------- */
.linkModal.linkModal-container {
  position: fixed;
  z-index: 9999;
  /* inset: 上  右  下  左 (ヘッダー分調整) */
  inset: calc(var(--⅝fem) * 5) 0 0 0;
  margin: auto;
  /* Grid */
  display: grid;
  place-content: center;
  place-items: center;
  transition: opacity 0.3s ease-in;
  max-width: 100%;
  max-height: 100%;
  opacity: 0;
}

/* オーバーレイ */
.linkModal.is-opened::before {
  content: "";
  position: fixed;
  z-index: -1;
  display: block;
  top: 0px;
  bottom: 0px;
  left: 0px;
  right: 0px;
  margin: auto;
  width: 100%;
  height: 100%;
  background: hsla(0, 0%, 0%, 0.7);
}

.linkModal.linkModal-container.is-opened {
  opacity: 1;
  pointer-events: auto;
}

.linkModal .linkModal-wrapper {
  position: relative;
  overflow: hidden;
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  background: var(--c-base, hsl(223, 6%, 100%));
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  overflow: hidden;
}

.linkModal-header {
  position: relative;
  z-index: 10;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 1.5rem;
  border-bottom: 1px solid #eee;
  background: #f8f9fa;
  flex-shrink: 0;
}

.linkModal .linkModal-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--c-silver, hsl(223, 6%, 75%));
}

.linkModal-content {
  padding: 1.5rem;
  overflow-y: auto;
  flex: 1;
  min-height: 0;
  position: relative;
}

/* モーダル内コンテンツの基本スタイル */
.linkModal-content * {
  max-width: 100% !important;
  box-sizing: border-box !important;
}

.linkModal-content img {
  width: auto !important;
  height: auto !important;
  max-width: 100% !important;
  display: block !important;
  margin: 0 auto !important;
}

.linkModal-content p,
.linkModal-content div {
  line-height: 1.6 !important;
  margin-bottom: 1rem !important;
}

.linkModal-loading {
  text-align: center;
  padding: 3rem 2rem;
  color: #666;
  font-size: 1rem;
}

.linkModal-error {
  text-align: center;
  padding: 3rem 2rem;
  color: #dc3545;
  font-size: 1rem;
  line-height: 1.5;
}

/* モーダルを閉じるボタン */
.linkModal.is-opened .linkModal-closer {
  position: absolute;
  top: -0.3em;
  right: 0.3em;
  margin: auto;
  display: inline-flex;
  justify-content: center;
  align-items: center;
  width: 0;
  height: 0;
  font-size: 2pc;
  color: #FFF;
}

.linkModal.is-opened .linkModal-closer::before {
  position: relative;
  top: 0px;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 0;
  height: 0;
  font-size: 100%;
  /* Material Symbols */
  font-family: 'Material Symbols Sharp';
  font-variation-settings:
    'FILL' 0,
    'wght' 200;
  content: "\\e5cd";
}

body.is-dialoged {
  overflow: hidden;
}
`;
document.head.appendChild(modalStyle);

  //既存のモーダルを削除する関数
  function removeExistingModal() {
    const existingModal = document.querySelector('.linkModal-container');
    if (existingModal) {
      existingModal.remove();
    }
  }

  //モーダルを作成する関数
  function createModal(width, height, title) {
    const modalHTML = `
    <div class="linkModal linkModal-container" style="width:${width}px; height:${height}px;">
      <div class="linkModal-wrapper">
        <div class="linkModal-header">
          <h3 class="linkModal-title">${title}</h3>
        </div>
        <div class="linkModal-content">
          <div class="linkModal-loading">読み込み中...</div>
        </div>
      </div>
      <button class="linkModal-closer" aria-label="閉じる"></button>
    </div>
    `;
    
    document.body.insertAdjacentHTML('beforeend', modalHTML);
    return document.querySelector('.linkModal-container:last-child');
  }

  //HTMLからbodyの内容とタイトルを抽出する関数(改良版)
  function extractContent(html) {
    console.log('Original HTML length:', html.length);
    
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    
    //タイトルを取得してサイト名をトリム
    let cleanTitle = '';
    const titleElement = doc.querySelector('title');
    if (titleElement) {
      const fullTitle = titleElement.textContent.trim();
      console.log('Original title:', fullTitle);

      const siteName = '- [サイト名.jp]';
      cleanTitle = fullTitle.replace(siteName, '').trim();
      
      console.log('Clean title:', cleanTitle);
    }
    
    //bodyの内容を取得
    const bodyContent = doc.body;
    let bodyHTML = '';
    
    if (bodyContent) {
      //不要な要素を削除
      const elementsToRemove = [
        'script',
        'noscript', 
        'style',
        '#wpadminbar',
        '.header',
        '.vessel .asleeve',
        '.footer',
        '.breadcrumb',
        '.post-edit-link',
        '.prxtPost',
        '.veu_contentAddSection',
        '.pageBtn-group'
      ];
      
      elementsToRemove.forEach(selector => {
        const elements = bodyContent.querySelectorAll(selector);
        elements.forEach(el => {
          console.log('Removing element:', el.tagName, el.className);
          el.remove();
        });
      });
      
      bodyHTML = bodyContent.innerHTML;
      console.log('Processed content length:', bodyHTML.length);
      console.log('Content preview:', bodyHTML.substring(0, 500));
    }
    
    return {
      title: cleanTitle,
      content: bodyHTML || html
    };
  }

  //コンテンツを読み込む関数(改良版)
  async function loadContent(url) {
    console.log('Loading content from:', url);
    
    try {
      const response = await fetch(url, {
        method: 'GET',
        headers: {
          'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        }
      });
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      const text = await response.text();
      console.log('Response received, length:', text.length);
      
      const extracted = extractContent(text);
      
      //コンテンツが空の場合のフォールバック
      if (!extracted.content || extracted.content.trim().length === 0) {
        console.log('Content is empty, using fallback');
        return {
          title: extracted.title,
          content: `
            <div style="padding: 2rem; text-align: center;">
              <p>コンテンツを表示できませんでした。</p>
              <a href="${url}" target="_blank" rel="noopener">新しいタブで開く</a>
            </div>
          `
        };
      }
      
      return extracted;
    } catch (error) {
      console.error('Content loading error:', error);
      return {
        title: 'エラー',
        content: `
          <div class="linkModal-error">
            <strong>コンテンツの読み込みに失敗しました</strong><br>
            エラー: ${error.message}<br><br>
            <a href="${url}" target="_blank" rel="noopener">新しいタブで開く</a>
          </div>
        `
      };
    }
  }

  //モーダルを閉じる関数
  function closeModal(modal) {
    modal.classList.remove('is-opened');
    document.body.classList.remove('is-dialoged');
    
    setTimeout(() => {
      modal.remove();
    }, 300);
  }

  //各リンクにイベントリスナーを追加
  arrModalLinks.forEach(function (modalLink) {
    modalLink.addEventListener('click', async function (e) {
      e.preventDefault();
      
      console.log('Modal link clicked:', this.href);

      //既存のモーダルを削除
      removeExistingModal();

      //data属性からサイズを取得(なければデフォルト値)
      const popWidth = parseInt(modalLink.getAttribute('data-popwidth')) || 800;
      const popHeight = parseInt(modalLink.getAttribute('data-popheight')) || 600;
      
      //仮のタイトルでモーダルを作成
      const tempTitle = '';
      console.log('Creating modal:', { width: popWidth, height: popHeight, title: tempTitle });

      //モーダルを作成
      const modal = createModal(popWidth, popHeight, tempTitle);
      const contentContainer = modal.querySelector('.linkModal-content');
      
      //モーダルを表示
      document.body.classList.add('is-dialoged');
      setTimeout(() => {
        modal.classList.add('is-opened');
      }, 10);

      //コンテンツを非同期で読み込み
      try {
        const result = await loadContent(this.href);
        console.log('Setting content to modal');
        contentContainer.innerHTML = result.content;
        
        //リンク先のタイトルでモーダルタイトルを更新
        const modalTitleElement = modal.querySelector('.linkModal-title');
        if (modalTitleElement && result.title) {
          modalTitleElement.textContent = result.title;
          console.log('Updated modal title to:', result.title);
        }

        //読み込み完了後に画像の遅延読み込みを無効化
        const lazyImages = contentContainer.querySelectorAll('img[loading="lazy"]');
        lazyImages.forEach(img => {
          img.removeAttribute('loading');
        });
        
        console.log('Content loaded successfully');
      } catch (error) {
        console.error('Error loading content:', error);
        contentContainer.innerHTML = `
          <div class="linkModal-error">
            <strong>コンテンツの読み込みに失敗しました</strong><br>
            エラー: ${error.message}<br><br>
            <a href="${this.href}" target="_blank" rel="noopener">新しいタブで開く</a>
          </div>
        `;
      }

      //閉じるボタンのイベントリスナー
      const closeBtn = modal.querySelector('.linkModal-closer');
      closeBtn.addEventListener('click', () => {
        closeModal(modal);
      });

      //背景クリックで閉じる
      modal.addEventListener('click', function (e) {
        if (e.target === modal) {
          closeModal(modal);
        }
      });

      //ESCキーで閉じる
      const escHandler = function (e) {
        if (e.key === 'Escape') {
          closeModal(modal);
          document.removeEventListener('keydown', escHandler);
        }
      };
      document.addEventListener('keydown', escHandler);
    });
  });

  console.log(`_link-openModal.js: ${arrModalLinks.length} modal links initialized`);
});

コードの詳細解説

1. 対象リンクの自動検出と変換

const config = {
  containerSelector: '.pt-cv-view.target-modal',
  linkSelector: 'a[href][class*="_self"]',
  defaultWidth: '800',
  defaultHeight: '500'
};

設定オブジェクトで対象範囲とデフォルトサイズを定義します。.target-modalクラスを持つコンテナ内の_selfクラス付きリンクが対象となります。

targetLinks.forEach(function(link) {
  if (href && link.classList.contains('_self')) {
    link.setAttribute('target', '_modal');
    link.setAttribute('data-popwidth', containerWidth);
    link.setAttribute('data-popheight', containerHeight);
  }
});

対象リンクにtarget="_modal"属性とサイズ情報を自動付与します。

2. モーダルのスタイリング

const modalStyle = document.createElement('style');
modalStyle.textContent = `
.linkModal.linkModal-container {
  position: fixed;
  z-index: 9999;
  inset: calc(var(--⅝fem) * 5) 0 0 0;
  /* ... */
}
`;
document.head.appendChild(modalStyle);

CSS-in-JSでモーダルのスタイルを動的に追加。外部CSSファイル不要で完全に自己完結しています。

3. コンテンツの取得と処理

async function loadContent(url) {
  const response = await fetch(url);
  const text = await response.text();
  const extracted = extractContent(text);
  return extracted;
}

fetch()APIでリンク先のHTMLを非同期取得します。

function extractContent(html) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, 'text/html');
  
  // タイトル抽出とサイト名トリム
  const titleElement = doc.querySelector('title');
  const siteName = '- [サイト名.jp]';
  cleanTitle = fullTitle.replace(siteName, '').trim();
  
  // 不要要素の削除
  const elementsToRemove = [
    '#wpadminbar', '.header', '.footer', '.breadcrumb'
  ];
}

HTMLをパースし、タイトルからサイト名を除去、不要な要素(ヘッダー、フッター等)を自動削除してコンテンツのみを抽出します。

カスタマイズ方法

1. デザインの変更

スタイル部分を修正してデザインをカスタマイズできます:

modalStyle.textContent = `
.linkModal-wrapper {
  background: #f0f0f0;                  /* 背景色変更 */
  border-radius: 12px;                      /* 角丸調整 */
  box-shadow: 0 8px 32px rgba(0,0,0,0.3); /* 影の調整 */
}

.linkModal-header {
  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); /* グラデーション */
  color: white;
}
`;

2. サイト名の変更

タイトル処理部分でサイト名を変更:

const siteName = '- [サイト名.jp]'; // ここを変更
cleanTitle = fullTitle.replace(siteName, '').trim();

3. 不要要素の追加削除

削除対象要素をカスタマイズ:

const elementsToRemove = [
  '#wpadminbar',
  '.header',
  '.footer', 
  '.sidebar',        // サイドバー追加
  '.comments',       // コメント欄追加
  '.related-posts'   // 関連記事追加
];

4. 対象リンクの条件変更

クラス名ベースではなく、URL パターンで判定したい場合:

// 現在の方式(クラス名ベース)
linkSelector: 'a[href][class*="_self"]',

// URL パターンベースに変更
const urlPattern = /\/post-\d+\/$/;
if (href && urlPattern.test(href)) {
  // 処理
}

5. モーダルサイズの個別設定

コンテナごとに異なるサイズを設定:

<!-- 大きなモーダル -->
<div class="pt-cv-view target-modal" data-modal-width="1200" data-modal-height="800">
  <!-- リンクここ -->
</div>

<!-- 小さなモーダル -->
<div class="pt-cv-view target-modal" data-modal-width="600" data-modal-height="400">
  <!-- リンクここ -->
</div>

実装時の注意点

同一オリジン制限

このスクリプトは同一ドメイン内のページにのみ対応しています。外部サイトを表示したい場合は<iframe>を使用する必要があります。

まとめ

この実装により、WordPressサイトや静的サイトで簡単にモーダル表示機能を追加できます。また、Content Viewsプラグイン等での記事一覧からでもモーダル表示することができるようになります。

コードは完全に自己完結しており、外部ライブラリに依存しないため、軽量で導入も簡単です。カスタマイズ性も高く、様々なサイトのデザインに合わせて調整可能です。

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?