<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>JavaScript &#8211; 小豬日常</title>
	<atom:link href="https://piglife.tw/tag/javascript/feed/" rel="self" type="application/rss+xml" />
	<link>https://piglife.tw</link>
	<description>Hello World，一個紀錄生活與學習的地方</description>
	<lastBuildDate>Fri, 02 Jan 2026 22:20:44 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://piglife.tw/wp-content/uploads/2017/10/cropped-logo-1-32x32.png</url>
	<title>JavaScript &#8211; 小豬日常</title>
	<link>https://piglife.tw</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>使用 Firebase Firestore 匯出抽獎資料為 CSV 檔案的實作說明</title>
		<link>https://piglife.tw/technical-notes/firebase-firestore-csv-export/</link>
					<comments>https://piglife.tw/technical-notes/firebase-firestore-csv-export/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Fri, 02 Jan 2026 22:20:44 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[CSV匯出]]></category>
		<category><![CDATA[Firebase]]></category>
		<category><![CDATA[Firestore]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[前端匯出]]></category>
		<category><![CDATA[資料分頁]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/firebase-firestore-csv-export/</guid>

					<description><![CDATA[本文說明如何結合 Firebase Firestore 與前端 JavaScript 實作抽獎資料的...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>本篇文章介紹如何在 PHP 環境中結合 Firebase Firestore，實作一個前端按鈕匯出抽獎資料為 CSV 檔案的功能。此功能適合需要將 Firestore 資料批次匯出，並提供給非技術人員下載的場景。讀者需具備基本 PHP 與 JavaScript 知識，並了解 Firebase Firestore 的基本操作。</p>
<h2 class="wp-block-heading">Firebase 初始化與匿名登入</h2>
<p>在前端使用 Firebase SDK，首先透過 <code>initializeApp</code> 初始化 Firebase，並使用匿名登入 (<code>signInAnonymously</code>) 取得存取 Firestore 的權限。這樣做可以避免在伺服器端直接操作 Firebase，減少安全風險。</p>
<pre><code class="lang-js language-js js">const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
await signInAnonymously(auth);
const db = getFirestore(app);</code></pre>
<h2 class="wp-block-heading">分頁抓取 Firestore 資料</h2>
<p>Firestore 資料量可能很大，因此使用分頁查詢避免一次讀取過多資料造成效能問題。透過 <code>orderBy(&quot;__name__&quot;)</code> 與 <code>startAfter</code> 搭配 <code>limit</code>，逐頁取得資料直到抓完為止。</p>
<pre><code class="lang-js language-js js">async function fetchAllEntriesPaged(db, pageSize = 500) {
  const colRef = collection(db, ENTRY_COLLECTION_PATH);
  let all = [];
  let lastDoc = null;
  while (true) {
    let q = query(colRef, orderBy(&quot;__name__&quot;), limit(pageSize));
    if (lastDoc) q = query(colRef, orderBy(&quot;__name__&quot;), startAfter(lastDoc), limit(pageSize));
    const snap = await getDocs(q);
    if (snap.empty) break;
    snap.forEach(d =&gt; all.push({ id: d.id, ...d.data() }));
    lastDoc = snap.docs[snap.docs.length - 1];
    if (snap.size &lt; pageSize) break;
  }
  return all;
}</code></pre>
<h2 class="wp-block-heading">CSV 格式處理與下載</h2>
<p>匯出 CSV 需要將資料轉成符合格式的字串，並處理特殊字元（如逗號、雙引號、換行）。<code>csvEscape</code> 函式負責這部分。下載時利用 Blob 物件與 <code>URL.createObjectURL</code> 產生可下載的連結。</p>
<pre><code class="lang-js language-js js">function csvEscape(val) {
  if (val === null || val === undefined) return &quot;&quot;;
  const s = String(val);
  if (/[&quot;,\n\r]/.test(s)) return `&quot;${s.replace(/&quot;/g, &#039;&quot;&quot;&#039;)}&quot;`;
  return s;
}

function downloadCSV(lines, filename) {
  const csv = &quot;\uFEFF&quot; + lines.join(&quot;\r\n&quot;);
  const blob = new Blob([csv], { type: &quot;text/csv;charset=utf-8;&quot; });
  const url = URL.createObjectURL(blob);
  const a = document.createElement(&quot;a&quot;);
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}</code></pre>
<h2 class="wp-block-heading">時間戳記格式轉換</h2>
<p>Firestore 的時間欄位格式特殊，可能是 Timestamp 物件或其他形式，透過 <code>tsToISO</code> 函式統一轉成 ISO 8601 字串，方便 CSV 讀取與後續處理。</p>
<pre><code class="lang-js language-js js">function tsToISO(v) {
  try {
    if (!v) return &quot;&quot;;
    if (typeof v.toDate === &quot;function&quot;) return v.toDate().toISOString();
    if (typeof v.seconds === &quot;number&quot;) return new Date(v.seconds * 1000).toISOString();
    return String(v);
  } catch {
    return &quot;&quot;;
  }
}</code></pre>
<h2 class="wp-block-heading">實作匯出流程</h2>
<p>按下「下載 CSV」按鈕後，觸發 <code>runExport</code> 函式，依序完成初始化、登入、資料分頁抓取、CSV 組合、下載檔案與狀態更新。過程中會禁用按鈕避免重複點擊，並顯示目前狀態。</p>
<pre><code class="lang-js language-js js">async function runExport() {
  $status.textContent = &quot;&quot;;
  $btn.disabled = true;
  $btn.textContent = &quot;匯出中...&quot;;
  try {
    const app = initializeApp(firebaseConfig);
    const auth = getAuth(app);
    await signInAnonymously(auth);
    const db = getFirestore(app);
    $status.textContent = &quot;正在抓取資料...&quot;;
    const rows = await fetchAllEntriesPaged(db, 500);
    if (!rows.length) {
      $status.textContent = &quot;沒有任何資料可匯出。&quot;;
      return;
    }
    const headers = [&quot;docId&quot;, &quot;name&quot;, &quot;classroom&quot;, &quot;teacher&quot;, &quot;entryDate&quot;, &quot;entryNumber&quot;, &quot;isWinner&quot;, &quot;drawTime&quot;, &quot;drawnBy&quot;, &quot;createdAt&quot;, &quot;updatedAt&quot;];
    const lines = [];
    lines.push(headers.map(csvEscape).join(&quot;,&quot;));
    for (const r of rows) {
      const line = [
        r.id ?? &quot;&quot;,
        r.name ?? &quot;&quot;,
        r.classroom ?? &quot;&quot;,
        r.teacher ?? &quot;&quot;,
        r.entryDate ?? &quot;&quot;,
        r.entryNumber ?? &quot;&quot;,
        (r.isWinner === true) ? &quot;true&quot; : &quot;false&quot;,
        r.drawTime ?? &quot;&quot;,
        r.drawnBy ?? &quot;&quot;,
        tsToISO(r.createdAt),
        tsToISO(r.updatedAt),
      ].map(csvEscape).join(&quot;,&quot;);
      lines.push(line);
    }
    const filename = `christmas_raffle_entries_${getTodayDate()}.csv`;
    downloadCSV(lines, filename);
    $status.textContent = `匯出完成：共 ${rows.length} 筆`;
  } catch (e) {
    console.error(e);
    $status.textContent = `匯出失敗：${e.message}`;
  } finally {
    $btn.disabled = false;
    $btn.textContent = &quot;⬇️ 下載 CSV&quot;;
  }
}</code></pre>
<h2 class="wp-block-heading">實際應用與延伸</h2>
<p>此實作適合用於管理後台快速匯出 Firestore 資料，方便非技術人員下載報表。未來可擴充支援更多資料欄位，或改用後端匯出以保護 Firebase 金鑰安全。也可以加入進度條或分頁匯出功能，提升使用體驗。</p>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>Firebase 配置需正確填寫並允許匿名登入。</li>
<li>Firestore 權限規則需設定允許讀取資料。</li>
<li>大量資料匯出時，注意瀏覽器記憶體限制與執行時間。</li>
<li>CSV 格式需妥善處理特殊字元避免格式錯亂。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">
&lt;?php
function at_christmas_export_menu_page()
{
    ?&gt;
    &lt;div class=&quot;wrap&quot;&gt;

&lt;h1&gt;匯出抽獎資料 CSV&lt;/h1&gt;

&lt;p&gt;
            &lt;button id=&quot;at-export-btn&quot; class=&quot;button button-primary&quot;&gt;⬇️ 下載 CSV&lt;/button&gt;
            &lt;span id=&quot;at-export-status&quot; style=&quot;margin-left:10px;&quot;&gt;&lt;/span&gt;
        &lt;/p&gt;
    &lt;/div&gt;

    &lt;script type=&quot;module&quot;&gt;
        import { initializeApp } from &quot;https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js&quot;;
        import { getAuth, signInAnonymously } from &quot;https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js&quot;;
        import {
            getFirestore,
            collection,
            query,
            getDocs,
            orderBy,
            limit,
            startAfter
        } from &quot;https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js&quot;;

        const firebaseConfig = {
            apiKey: &quot;YOUR_API_KEY&quot;,
            authDomain: &quot;YOUR_AUTH_DOMAIN&quot;,
            projectId: &quot;YOUR_PROJECT_ID&quot;,
            storageBucket: &quot;YOUR_STORAGE_BUCKET&quot;,
            messagingSenderId: &quot;YOUR_MESSAGING_SENDER_ID&quot;,
            appId: &quot;YOUR_APP_ID&quot;,
            measurementId: &quot;YOUR_MEASUREMENT_ID&quot;
        };

        const projectId = firebaseConfig.projectId || &quot;YOUR_PROJECT_ID&quot;;
        const ENTRY_COLLECTION_PATH = `artifacts/${projectId}/public/data/christmas_raffle_entries`;

        const $btn = document.getElementById(&quot;at-export-btn&quot;);
        const $status = document.getElementById(&quot;at-export-status&quot;);

        function getTodayDate() {
            const now = new Date();
            const y = now.getFullYear();
            const m = String(now.getMonth() + 1).padStart(2, &quot;0&quot;);
            const d = String(now.getDate()).padStart(2, &quot;0&quot;);
            return `${y}-${m}-${d}`;
        }

        function csvEscape(val) {
            if (val === null || val === undefined) return &quot;&quot;;
            const s = String(val);
            if (/[&quot;,\n\r]/.test(s)) return `&quot;${s.replace(/&quot;/g, &#039;&quot;&quot;&#039;)}&quot;`;
            return s;
        }

        function tsToISO(v) {
            try {
                if (!v) return &quot;&quot;;
                if (typeof v.toDate === &quot;function&quot;) return v.toDate().toISOString();
                if (typeof v.seconds === &quot;number&quot;) return new Date(v.seconds * 1000).toISOString();
                return String(v);
            } catch {
                return &quot;&quot;;
            }
        }

        async function fetchAllEntriesPaged(db, pageSize = 500) {
            const colRef = collection(db, ENTRY_COLLECTION_PATH);

            let all = [];
            let lastDoc = null;

            while (true) {
                let q = query(colRef, orderBy(&quot;__name__&quot;), limit(pageSize));
                if (lastDoc) q = query(colRef, orderBy(&quot;__name__&quot;), startAfter(lastDoc), limit(pageSize));

                const snap = await getDocs(q);
                if (snap.empty) break;

                snap.forEach(d =&gt; all.push({ id: d.id, ...d.data() }));
                lastDoc = snap.docs[snap.docs.length - 1];

                if (snap.size &lt; pageSize) break;
            }
            return all;
        }

        function downloadCSV(lines, filename) {
            const csv = &quot;\uFEFF&quot; + lines.join(&quot;\r\n&quot;); // BOM for Excel
            const blob = new Blob([csv], { type: &quot;text/csv;charset=utf-8;&quot; });
            const url = URL.createObjectURL(blob);
            const a = document.createElement(&quot;a&quot;);
            a.href = url;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            a.remove();
            URL.revokeObjectURL(url);
        }

        async function runExport() {
            $status.textContent = &quot;&quot;;
            $btn.disabled = true;
            $btn.textContent = &quot;匯出中...&quot;;

            try {
                const app = initializeApp(firebaseConfig);
                const auth = getAuth(app);
                await signInAnonymously(auth);

                const db = getFirestore(app);

                $status.textContent = &quot;正在抓取資料...&quot;;
                const rows = await fetchAllEntriesPaged(db, 500);

                if (!rows.length) {
                    $status.textContent = &quot;沒有任何資料可匯出。&quot;;
                    return;
                }

                const headers = [
                    &quot;docId&quot;,
                    &quot;name&quot;,
                    &quot;classroom&quot;,
                    &quot;teacher&quot;,
                    &quot;entryDate&quot;,
                    &quot;entryNumber&quot;,
                    &quot;isWinner&quot;,
                    &quot;drawTime&quot;,
                    &quot;drawnBy&quot;,
                    &quot;createdAt&quot;,
                    &quot;updatedAt&quot;
                ];

                const lines = [];
                lines.push(headers.map(csvEscape).join(&quot;,&quot;));

                for (const r of rows) {
                    const line = [
                        r.id ?? &quot;&quot;,
                        r.name ?? &quot;&quot;,
                        r.classroom ?? &quot;&quot;,
                        r.teacher ?? &quot;&quot;,
                        r.entryDate ?? &quot;&quot;,
                        r.entryNumber ?? &quot;&quot;,
                        (r.isWinner === true) ? &quot;true&quot; : &quot;false&quot;,
                        r.drawTime ?? &quot;&quot;,
                        r.drawnBy ?? &quot;&quot;,
                        tsToISO(r.createdAt),
                        tsToISO(r.updatedAt),
                    ].map(csvEscape).join(&quot;,&quot;);
                    lines.push(line);
                }

                const filename = `christmas_raffle_entries_${getTodayDate()}.csv`;
                downloadCSV(lines, filename);

                $status.textContent = `匯出完成：共 ${rows.length} 筆`;
            } catch (e) {
                console.error(e);
                $status.textContent = `匯出失敗：${e.message}`;
            } finally {
                $btn.disabled = false;
                $btn.textContent = &quot;⬇️ 下載 CSV&quot;;
            }
        }

        $btn.addEventListener(&quot;click&quot;, runExport);
    &lt;/script&gt;
&lt;?php
}
?&gt;</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/firebase-firestore-csv-export/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>JavaScript 實作禁止右鍵選單的簡易方法</title>
		<link>https://piglife.tw/technical-notes/javascript-disable-right-click/</link>
					<comments>https://piglife.tw/technical-notes/javascript-disable-right-click/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Tue, 30 Dec 2025 22:20:29 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[事件監聽]]></category>
		<category><![CDATA[前端防護]]></category>
		<category><![CDATA[右鍵禁用]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/javascript-disable-right-click/</guid>

					<description><![CDATA[介紹如何使用 JavaScript 監聽 contextmenu 事件並阻止預設右鍵選單，提供簡單有...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在某些網頁應用中，開發者可能希望防止使用者透過滑鼠右鍵開啟選單，以避免內容被輕易複製或操作。這段程式碼提供了一個簡單且不影響其他滑鼠功能的解決方案，適合有基礎 JavaScript 知識且想快速實作右鍵禁用的工程師或自學者。</p>
<h2 class="wp-block-heading">事件監聽與右鍵選單阻擋</h2>
<p>核心概念是監聽瀏覽器的 <code>contextmenu</code> 事件，該事件會在使用者嘗試開啟右鍵選單時觸發。透過呼叫 <code>event.preventDefault()</code>，可以阻止預設行為，也就是禁止顯示右鍵選單。</p>
<h3 class="wp-block-heading">重要程式碼片段</h3>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&quot;contextmenu&quot;, function (e) {
    e.preventDefault();
});</code></pre>
<p>這段程式碼綁定全域的 <code>contextmenu</code> 事件監聽器，當事件發生時直接阻止預設動作。</p>
<h2 class="wp-block-heading">為什麼用立即執行函式包裹？</h2>
<p>使用立即執行函式 (IIFE) 可以避免全域變數污染，確保程式碼在嚴格模式下運行 (<code>&quot;use strict&quot;</code>)，增加程式的穩定性與安全性。</p>
<h2 class="wp-block-heading">實務應用與注意事項</h2>
<ul>
<li>此方法只禁止右鍵選單，不會影響左鍵點擊、文字選取或快捷鍵操作，對使用者體驗影響較小。</li>
<li>但此防護並非絕對安全，仍可被瀏覽器開發者工具或其他方式繞過，適合用於降低一般使用者誤操作。</li>
<li>若需更嚴格的內容保護，建議搭配其他前端或後端策略。</li>
</ul>
<h2 class="wp-block-heading">延伸優化方向</h2>
<ul>
<li>可針對特定元素綁定 <code>contextmenu</code> 事件，避免全域禁用造成使用不便。</li>
<li>結合提示訊息告知使用者右鍵功能已被禁用，提高使用者理解。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-javascript language-javascript javascript">(function () {
    &quot;use strict&quot;;

    // 只鎖右鍵選單（不影響左鍵、選字、快捷鍵）
    document.addEventListener(&quot;contextmenu&quot;, function (e) {
        e.preventDefault();
    });
})();</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/javascript-disable-right-click/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>修正 YouTube 影片循環播放的 iframe 參數設定技巧</title>
		<link>https://piglife.tw/technical-notes/youtube-iframe-loop-fix/</link>
					<comments>https://piglife.tw/technical-notes/youtube-iframe-loop-fix/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Fri, 26 Dec 2025 22:20:41 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[iframe]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[YouTube]]></category>
		<category><![CDATA[前端優化]]></category>
		<category><![CDATA[影片循環]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/youtube-iframe-loop-fix/</guid>

					<description><![CDATA[本篇介紹如何透過 JavaScript 自動修正 YouTube iframe 的 URL 參數，確...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在網頁中嵌入 YouTube 影片時，若希望影片能夠無限循環播放，通常需要在 iframe 的 URL 中加入特定參數。這段程式碼解決了直接設定 loop 參數無法生效的問題，適合需要動態調整或修正第三方影片嵌入參數的前端工程師與自學者。</p>
<h2 class="wp-block-heading">問題背景與解決方案</h2>
<p>YouTube 的 iframe 影片循環播放並非只靠 <code>loop=1</code> 參數即可生效，還必須同時指定 <code>playlist</code> 參數為影片 ID。這段程式碼會自動偵測所有帶有 <code>youtube-loop</code> 類別的 iframe，並補上正確的 <code>loop</code> 與 <code>playlist</code> 參數。</p>
<h3 class="wp-block-heading">取得並解析 iframe 的 src</h3>
<p>程式碼先取得所有符合條件的 iframe，並將 <code>src</code> 中可能被轉譯的 <code>&amp;amp;</code> 轉回 <code>&amp;amp;</code>，確保 URL 正確解析。</p>
<pre><code class="lang-javascript language-javascript javascript">const rawSrc = iframe.getAttribute(&#039;src&#039;);
const src = rawSrc.replace(/&amp;amp;/g, &#039;&amp;&#039;);</code></pre>
<h3 class="wp-block-heading">取得影片 ID</h3>
<p>透過字串切割方式，從 src 中擷取影片 ID，這是設定 playlist 參數的關鍵。</p>
<pre><code class="lang-javascript language-javascript javascript">const videoId = src.split(&#039;/embed/&#039;)[1]?.split(&#039;?&#039;)[0];</code></pre>
<h3 class="wp-block-heading">設定 loop 與 playlist 參數</h3>
<p>利用 URL 物件操作，設定 <code>loop=1</code> 與 <code>playlist=影片ID</code>，並避免重複設定造成無限迴圈。</p>
<pre><code class="lang-javascript language-javascript javascript">const url = new URL(src);
url.searchParams.set(&#039;loop&#039;, &#039;1&#039;);
url.searchParams.set(&#039;playlist&#039;, videoId);
const newSrc = url.toString();
if (newSrc !== src) iframe.setAttribute(&#039;src&#039;, newSrc);</code></pre>
<h2 class="wp-block-heading">實務應用與優化建議</h2>
<ul>
<li>此方法適合在頁面載入後或動態插入 iframe 後呼叫，確保所有影片都能正確設定循環播放。</li>
<li>透過多次延遲呼叫（setTimeout）處理 builder 或 lazy load 產生的 iframe。</li>
<li>若有大量 iframe，可考慮改用 MutationObserver 監控 DOM 變化，提高效能與即時性。</li>
</ul>
<h2 class="wp-block-heading">常見問題與坑點</h2>
<ul>
<li>直接在 iframe URL 加 <code>loop=1</code> 不加 <code>playlist</code>，影片不會循環。</li>
<li>URL 中的 <code>&amp;amp;</code> 需先轉回 <code>&amp;amp;</code>，否則 URL 解析會錯誤。</li>
<li>避免重複設定 src，否則會造成 iframe 不斷重新載入。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-javascript language-javascript javascript">(function () {
  function fixYouTubeLoop() {
    document.querySelectorAll(&#039;iframe.youtube-loop&#039;).forEach((iframe) =&gt; {
      const rawSrc = iframe.getAttribute(&#039;src&#039;);
      if (!rawSrc) return;

      // 你的 src 可能含有 &amp;amp;，先轉回正常的 &amp;
      const src = rawSrc.replace(/&amp;amp;/g, &#039;&amp;&#039;);

      if (!src.includes(&#039;youtube.com/embed/&#039;)) return;

      const videoId = src.split(&#039;/embed/&#039;)[1]?.split(&#039;?&#039;)[0];
      if (!videoId) return;

      const url = new URL(src);
      url.searchParams.set(&#039;loop&#039;, &#039;1&#039;);
      url.searchParams.set(&#039;playlist&#039;, videoId);

      const newSrc = url.toString();

      // 避免無限重設
      if (newSrc !== src) iframe.setAttribute(&#039;src&#039;, newSrc);
    });
  }

  document.addEventListener(&#039;DOMContentLoaded&#039;, fixYouTubeLoop);

  // 若是 builder / lazy load 後面才插入 iframe，再補一次
  setTimeout(fixYouTubeLoop, 800);
  setTimeout(fixYouTubeLoop, 2000);
})();</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/youtube-iframe-loop-fix/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WooCommerce 後台商品列表新增複製購物車連結並支援優惠券輸入功能</title>
		<link>https://piglife.tw/technical-notes/woocommerce-copy-cart-link-coupon/</link>
					<comments>https://piglife.tw/technical-notes/woocommerce-copy-cart-link-coupon/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Tue, 23 Dec 2025 22:21:03 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[woocommerce]]></category>
		<category><![CDATA[優惠券]]></category>
		<category><![CDATA[剪貼簿操作]]></category>
		<category><![CDATA[後台自訂欄位]]></category>
		<category><![CDATA[複製連結]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/woocommerce-copy-cart-link-coupon/</guid>

					<description><![CDATA[介紹如何在 WooCommerce 後台商品列表新增一欄複製加入購物車連結的按鈕，並支援彈窗輸入優惠...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在 WooCommerce 後台商品列表中，管理者常需要快速取得商品的「加入購物車」連結以便分享或推廣。這段程式碼實作了在商品列表中新增一欄「複製課程連結」按鈕，點擊後可彈出視窗讓使用者輸入優惠券代碼，並將優惠券參數附加到連結中，最後將完整連結複製到剪貼簿。這對於需要快速產生帶優惠券的購物車連結的電商管理者非常實用。</p>
<h2 class="wp-block-heading">新增自訂欄位顯示複製按鈕</h2>
<p>利用 <code>manage_edit-product_columns</code> 過濾器新增一欄「複製課程連結」，並透過 <code>manage_product_posts_custom_column</code> 動作在該欄位輸出一個帶有商品加入購物車 URL 的按鈕。URL 是以網站首頁 URL 加上 <code>/cart/?add-to-cart=商品ID</code> 組成。</p>
<pre><code class="lang-php language-php php">add_filter(&#039;manage_edit-product_columns&#039;, &#039;add_copy_link_column&#039;);
function add_copy_link_column($columns) {
    $columns[&#039;copy_add_to_cart&#039;] = __(&#039;複製課程連結&#039;, &#039;woocommerce&#039;);
    return $columns;
}

add_action(&#039;manage_product_posts_custom_column&#039;, &#039;show_copy_link_column_content&#039;, 10, 2);
function show_copy_link_column_content($column, $post_id) {
    if ($column === &#039;copy_add_to_cart&#039;) {
        $product_id = (int) $post_id;
        $site_url = my_copy_cart_site_url();
        $add_to_cart_url = $site_url . &#039;/cart/?add-to-cart=&#039; . $product_id;
        echo &#039;&lt;button type=&quot;button&quot; class=&quot;button copy-cart-link&quot; data-url=&quot;&#039; . esc_attr($add_to_cart_url) . &#039;&quot; title=&quot;點擊複製連結&quot;&gt;複製&lt;/button&gt;&#039;;
    }
}</code></pre>
<h2 class="wp-block-heading">客製化 JavaScript 處理複製邏輯與優惠券輸入</h2>
<p>在後台商品列表頁尾插入 JavaScript 和 CSS，當使用者點擊「複製」按鈕時：</p>
<ol>
<li>透過 <code>window.prompt</code> 詢問是否輸入優惠券代碼，可留空。</li>
<li>根據輸入結果組合最終 URL，若有優惠券則加上 <code>&amp;wt_coupon=優惠券代碼</code>。</li>
<li>使用 Clipboard API 複製連結，若不支援則使用傳統 <code>execCommand</code> 備援。</li>
<li>複製成功會顯示成功提示，失敗則顯示錯誤提示並提供手動複製。</li>
</ol>
<pre><code class="lang-js language-js js">function buildFinalUrl(baseUrl) {
    var coupon = window.prompt(&#039;是否要加入優惠券？\n(可留空，直接按確定即可)&#039;, &#039;&#039;);
    if (coupon === null) return baseUrl;
    coupon = String(coupon || &#039;&#039;).trim();
    if (!coupon) return baseUrl;
    var joiner = (baseUrl.indexOf(&#039;?&#039;) === -1) ? &#039;?&#039; : &#039;&amp;&#039;;
    return baseUrl + joiner + &#039;wt_coupon=&#039; + encodeURIComponent(coupon);
}

$(document).on(&#039;click&#039;, &#039;.copy-cart-link&#039;, function(e) {
    e.preventDefault();
    var button = $(this);
    var baseUrl = button.data(&#039;url&#039;);
    var originalHtml = button.html();
    if (button.hasClass(&#039;copying&#039;)) return;
    button.addClass(&#039;copying&#039;);
    var finalUrl = buildFinalUrl(baseUrl);
    if (navigator.clipboard &amp;&amp; window.isSecureContext) {
        navigator.clipboard.writeText(finalUrl).then(function() {
            showCopySuccess(button, originalHtml);
        }).catch(function() {
            fallbackCopyTextToClipboard(finalUrl, button, originalHtml);
        });
    } else {
        fallbackCopyTextToClipboard(finalUrl, button, originalHtml);
    }
});</code></pre>
<h2 class="wp-block-heading">快速動作連結整合</h2>
<p>除了欄位按鈕外，也在商品標題下方的快速動作連結加入「複製購物車連結」，提升操作便利性。此連結同樣帶有商品加入購物車的 URL，並套用相同的 JavaScript 行為。</p>
<pre><code class="lang-php language-php php">add_filter(&#039;post_row_actions&#039;, &#039;add_copy_link_quick_action&#039;, 10, 2);
function add_copy_link_quick_action($actions, $post) {
    if ($post-&gt;post_type === &#039;product&#039;) {
        $product_id = (int) $post-&gt;ID;
        $site_url = my_copy_cart_site_url();
        $add_to_cart_url = $site_url . &#039;/cart/?add-to-cart=&#039; . $product_id;
        $actions[&#039;copy_cart_link&#039;] = &#039;&lt;a href=&quot;#&quot; class=&quot;copy-cart-link&quot; data-url=&quot;&#039; . esc_attr($add_to_cart_url) . &#039;&quot; style=&quot;color:#0073aa;&quot;&gt;📋 複製購物車連結&lt;/a&gt;&#039;;
    }
    return $actions;
}</code></pre>
<h2 class="wp-block-heading">實務應用與優化建議</h2>
<p>此功能適合需要快速產生帶優惠券的購物車連結的 WooCommerce 商店管理員，方便推廣或客服回覆。未來可優化：</p>
<ul>
<li>將網站 URL 改為動態取得，避免硬編碼。</li>
<li>增加複製成功的視覺動畫提升 UX。</li>
<li>支援多種優惠券參數或其他自訂參數。</li>
<li>對於大量商品列表，可考慮優化前端效能。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
/**
 * WooCommerce 後台商品列表添加複製 add-to-cart 連結的功能 + 可選優惠券
 * 適用於 WPCode（PHP Snippet / Run Everywhere 或 Admin Only）
 *
 * 功能：
 * - 商品列表新增一欄「複製課程連結」
 * - 點按後彈出視窗詢問優惠券（可留空）
 * - 若有填，複製的 URL 會加上：&amp;wt_coupon=COUPON
 * - 同時支援「快速動作」複製
 */

/** 你網站的網域（建議改成動態，不用寫死） */
function my_copy_cart_site_url() {
    return home_url();
}

// 在商品列表中添加自定義列
add_filter(&#039;manage_edit-product_columns&#039;, &#039;add_copy_link_column&#039;);
function add_copy_link_column($columns) {
    $columns[&#039;copy_add_to_cart&#039;] = __(&#039;複製課程連結&#039;, &#039;woocommerce&#039;);
    return $columns;
}

// 顯示列內容
add_action(&#039;manage_product_posts_custom_column&#039;, &#039;show_copy_link_column_content&#039;, 10, 2);
function show_copy_link_column_content($column, $post_id) {
    if ($column === &#039;copy_add_to_cart&#039;) {
        $product_id = (int) $post_id;

        $site_url = my_copy_cart_site_url();
        $add_to_cart_url = $site_url . &#039;/cart/?add-to-cart=&#039; . $product_id;

        echo &#039;&lt;button type=&quot;button&quot; class=&quot;button copy-cart-link&quot; data-url=&quot;&#039; . esc_attr($add_to_cart_url) . &#039;&quot; title=&quot;點擊複製連結&quot;&gt;
                 複製
              &lt;/button&gt;&#039;;
    }
}

// 添加 JavaScript 和 CSS
add_action(&#039;admin_footer&#039;, &#039;copy_link_admin_script&#039;);
function copy_link_admin_script() {
    $screen = get_current_screen();
    if (!$screen || $screen-&gt;id !== &#039;edit-product&#039;) {
        return;
    }
    ?&gt;

&lt;style&gt;
        .copy-cart-link {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            padding: 4px 8px;
            font-size: 12px;
            line-height: 1.4;
            border-radius: 3px;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        .copy-cart-link:hover {
            background-color: #000000;
            color: white;
            transform: translateY(-1px);
        }
        .copy-cart-link .dashicons {
            font-size: 14px;
            width: 14px;
            height: 14px;
        }
        .column-copy_add_to_cart {
            width: 110px;
            text-align: center;
        }
        @media screen and (max-width: 782px) {
            .copy-cart-link {
                padding: 6px 10px;
                font-size: 13px;
            }
        }
    &lt;/style&gt;

    &lt;script type=&quot;text/javascript&quot;&gt;
        jQuery(document).ready(function($) {

            // 組裝最終要複製的 URL（可選 wt_coupon）
            function buildFinalUrl(baseUrl) {
                // 1) 詢問優惠券（可留空）
                var coupon = window.prompt(&#039;是否要加入優惠券？\n(可留空，直接按確定即可)&#039;, &#039;&#039;);

                // 使用者按取消 -&gt; 仍照原本複製（你也可以改成 return null 代表不複製）
                if (coupon === null) {
                    return baseUrl;
                }

                coupon = String(coupon || &#039;&#039;).trim();
                if (!coupon) {
                    return baseUrl;
                }

                // 2) 已有 ?add-to-cart=...，直接加 &amp;wt_coupon=
                //    若未來 baseUrl 可能沒有 query，這裡也做保險處理
                var joiner = (baseUrl.indexOf(&#039;?&#039;) === -1) ? &#039;?&#039; : &#039;&amp;&#039;;
                return baseUrl + joiner + &#039;wt_coupon=&#039; + encodeURIComponent(coupon);
            }

            $(document).on(&#039;click&#039;, &#039;.copy-cart-link&#039;, function(e) {
                e.preventDefault();

                var button = $(this);
                var baseUrl = button.data(&#039;url&#039;);
                var originalHtml = button.html();

                if (button.hasClass(&#039;copying&#039;)) return;
                button.addClass(&#039;copying&#039;);

                var finalUrl = buildFinalUrl(baseUrl);

                // 若你希望「按取消就不複製」，把 buildFinalUrl 的取消行為改成 return null
                // 然後這裡加上：
                // if (!finalUrl) { button.removeClass(&#039;copying&#039;); return; }

                if (navigator.clipboard &amp;&amp; window.isSecureContext) {
                    navigator.clipboard.writeText(finalUrl).then(function() {
                        showCopySuccess(button, originalHtml);
                    }).catch(function(err) {
                        console.error(&#039;Copy failed: &#039;, err);
                        fallbackCopyTextToClipboard(finalUrl, button, originalHtml);
                    });
                } else {
                    fallbackCopyTextToClipboard(finalUrl, button, originalHtml);
                }
            });

            function fallbackCopyTextToClipboard(text, button, originalHtml) {
                var textArea = document.createElement(&quot;textarea&quot;);
                textArea.value = text;

                textArea.style.top = &quot;0&quot;;
                textArea.style.left = &quot;0&quot;;
                textArea.style.position = &quot;fixed&quot;;
                textArea.style.opacity = &quot;0&quot;;
                textArea.style.pointerEvents = &quot;none&quot;;

                document.body.appendChild(textArea);
                textArea.focus();
                textArea.select();

                try {
                    var successful = document.execCommand(&#039;copy&#039;);
                    if (successful) {
                        showCopySuccess(button, originalHtml);
                    } else {
                        showCopyError(button, originalHtml, text);
                    }
                } catch (err) {
                    console.error(&#039;Copy failed: &#039;, err);
                    showCopyError(button, originalHtml, text);
                }

                document.body.removeChild(textArea);
            }

            function showCopySuccess(button, originalHtml) {
                button.removeClass(&#039;copying&#039;).addClass(&#039;copied&#039;);
                button.html(&#039;&lt;span class=&quot;dashicons dashicons-yes&quot;&gt;&lt;/span&gt; 已複製&#039;);

                setTimeout(function() {
                    button.removeClass(&#039;copied&#039;);
                    button.html(originalHtml);
                }, 2500);
            }

            function showCopyError(button, originalHtml, text) {
                button.removeClass(&#039;copying&#039;);

                var tooltip = $(&#039;&lt;div class=&quot;copy-error-tooltip&quot; style=&quot;position:absolute;background:#333;color:#fff;padding:8px 12px;border-radius:4px;font-size:12px;z-index:9999;max-width:320px;word-break:break-all;&quot;&gt;複製失敗，請手動複製：&lt;br&gt;&#039; + text + &#039;&lt;/div&gt;&#039;);
                $(&#039;body&#039;).append(tooltip);

                var buttonOffset = button.offset();
                tooltip.css({
                    top: buttonOffset.top - tooltip.outerHeight() - 5,
                    left: buttonOffset.left
                });

                setTimeout(function() { tooltip.remove(); }, 5000);

                tooltip.on(&#039;click&#039;, function() {
                    if (navigator.clipboard) navigator.clipboard.writeText(text);
                    $(this).remove();
                });
            }

        });
    &lt;/script&gt;
    &lt;?php
}

// 可選：添加快速動作連結（在商品名稱下方）
add_filter(&#039;post_row_actions&#039;, &#039;add_copy_link_quick_action&#039;, 10, 2);
function add_copy_link_quick_action($actions, $post) {
    if ($post-&gt;post_type === &#039;product&#039;) {
        $product_id = (int) $post-&gt;ID;

        $site_url = my_copy_cart_site_url();
        $add_to_cart_url = $site_url . &#039;/cart/?add-to-cart=&#039; . $product_id;

        $actions[&#039;copy_cart_link&#039;] = &#039;&lt;a href=&quot;#&quot; class=&quot;copy-cart-link&quot; data-url=&quot;&#039; . esc_attr($add_to_cart_url) . &#039;&quot; style=&quot;color:#0073aa;&quot;&gt;📋 複製購物車連結&lt;/a&gt;&#039;;
    }
    return $actions;
}
</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/woocommerce-copy-cart-link-coupon/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>使用純 JavaScript 實作語言切換下拉選單的開關功能</title>
		<link>https://piglife.tw/technical-notes/javascript-language-dropdown-toggle/</link>
					<comments>https://piglife.tw/technical-notes/javascript-language-dropdown-toggle/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 10:24:01 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[JavaScript]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/javascript-language-dropdown-toggle/</guid>

					<description><![CDATA[本文介紹如何使用純 JavaScript 實作語言切換下拉選單的開關與關閉功能，包含點擊按鈕切換、點...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在多語系網站中，語言切換器常以下拉選單呈現。這段程式碼示範如何用純 JavaScript 實作語言切換下拉選單的開關與關閉行為，適合需要自訂互動邏輯且不依賴外部函式庫的前端工程師或自學者。</p>
<h2 class="wp-block-heading">元素選取與初始檢查</h2>
<p>首先透過 <code>document.querySelector</code> 取得包裹整個語言切換器的容器 <code>.lang-wrapper</code>，若找不到則直接結束，避免後續執行錯誤。接著從 wrapper 中取得觸發按鈕 <code>.lang-btn</code> 和下拉選單 <code>nav[role=&quot;navigation&quot;]</code>，同樣檢查是否存在。</p>
<pre><code class="lang-javascript language-javascript javascript">const wrapper = document.querySelector(&#039;.lang-wrapper&#039;);
if (!wrapper) return;

const btn = wrapper.querySelector(&#039;.lang-btn&#039;);
const nav = wrapper.querySelector(&#039;nav[role=&quot;navigation&quot;]&#039;);
if (!btn || !nav) return;</code></pre>
<p>這樣的防呆設計確保程式碼安全執行。</p>
<h2 class="wp-block-heading">點擊按鈕切換下拉選單</h2>
<p>為按鈕綁定點擊事件，點擊時阻止事件冒泡，避免觸發 document 的點擊事件導致下拉選單立即關閉。接著透過 <code>classList.toggle</code> 切換下拉選單的 <code>is-open</code> 樣式類別，達成開關效果。</p>
<pre><code class="lang-javascript language-javascript javascript">btn.addEventListener(&#039;click&#039;, function (e) {
  e.stopPropagation();
  nav.classList.toggle(&#039;is-open&#039;);
});</code></pre>
<h2 class="wp-block-heading">點擊外部區域關閉下拉選單</h2>
<p>監聽整個文件的點擊事件，判斷點擊目標是否在 wrapper 範圍內，若不在則移除 <code>is-open</code>，關閉下拉選單。這是常見的 UX 行為，避免下拉選單在使用者點擊其他區域時仍保持開啟。</p>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&#039;click&#039;, function (e) {
  if (!wrapper.contains(e.target)) {
    nav.classList.remove(&#039;is-open&#039;);
  }
});</code></pre>
<h2 class="wp-block-heading">按下 ESC 鍵關閉下拉選單</h2>
<p>為提升無障礙與使用便利性，監聽鍵盤事件，當使用者按下 ESC 鍵時，也會關閉下拉選單。</p>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&#039;keydown&#039;, function (e) {
  if (e.key === &#039;Escape&#039;) {
    nav.classList.remove(&#039;is-open&#039;);
  }
});</code></pre>
<h2 class="wp-block-heading">實務應用與優化方向</h2>
<p>此實作不依賴任何框架，適用於輕量網站。若要擴充，可加入鍵盤導覽支援（如方向鍵選擇）、ARIA 屬性強化無障礙，或改用事件代理提升效能。此外，CSS 動畫搭配 <code>is-open</code> 類別能帶來更好的使用體驗。</p>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&#039;DOMContentLoaded&#039;, function () {
  const wrapper = document.querySelector(&#039;.lang-wrapper&#039;);
  if (!wrapper) return;

  const btn = wrapper.querySelector(&#039;.lang-btn&#039;);
  const nav = wrapper.querySelector(&#039;nav[role=&quot;navigation&quot;]&#039;);
  if (!btn || !nav) return;

  btn.addEventListener(&#039;click&#039;, function (e) {
    e.stopPropagation();
    nav.classList.toggle(&#039;is-open&#039;);
  });

  document.addEventListener(&#039;click&#039;, function (e) {
    if (!wrapper.contains(e.target)) {
      nav.classList.remove(&#039;is-open&#039;);
    }
  });

  document.addEventListener(&#039;keydown&#039;, function (e) {
    if (e.key === &#039;Escape&#039;) {
      nav.classList.remove(&#039;is-open&#039;);
    }
  });
});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/javascript-language-dropdown-toggle/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>利用 JavaScript 自動為當前頁面側邊欄連結加上 active 樣式</title>
		<link>https://piglife.tw/technical-notes/js-active-link-current-path/</link>
					<comments>https://piglife.tw/technical-notes/js-active-link-current-path/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 10:22:58 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[active 樣式]]></category>
		<category><![CDATA[JavaScript]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/js-active-link-current-path/</guid>

					<description><![CDATA[介紹如何用 JavaScript 取得當前頁面路徑並與側邊欄連結比對，動態為正確連結加上 activ...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在多頁面網站中，常見需求是讓側邊欄或導覽列中的連結能自動標示當前所在頁面，提升使用者導覽體驗。這段程式碼示範如何透過 JavaScript 取得當前網頁路徑，並比對側邊欄所有連結的 href，對應到正確連結時自動加上「active」CSS 類別。適合需要動態控制導覽狀態的前端工程師或自學者。</p>
<h2 class="wp-block-heading">取得並標準化當前頁面路徑</h2>
<p>首先，我們透過 <code>window.location.pathname</code> 取得目前頁面的路徑，並轉成小寫以避免大小寫不一致問題。接著使用正則表達式移除路徑尾端多餘的斜線，確保 <code>/a/b/</code> 與 <code>/a/b</code> 被視為相同路徑。若路徑為空字串，則設定為根目錄 <code>/</code>。</p>
<pre><code class="lang-javascript language-javascript javascript">let currentPath = window.location.pathname.toLowerCase();
currentPath = currentPath.replace(/\/+$|\/$/, &quot;&quot;);
if (currentPath === &quot;&quot;) currentPath = &quot;/&quot;;</code></pre>
<h2 class="wp-block-heading">選取側邊欄連結並逐一比對</h2>
<p>使用 <code>document.querySelectorAll</code> 選取所有帶有 <code>.anchor-links .link</code> 的連結元素。對每個連結，我們先取得 <code>href</code> 屬性，並使用 <code>URL</code> 物件將相對路徑轉成絕對路徑，確保比對時不會因為相對路徑造成錯誤。</p>
<pre><code class="lang-javascript language-javascript javascript">const links = document.querySelectorAll(&quot;.anchor-links .link&quot;);
links.forEach(link =&gt; {
  const href = link.getAttribute(&quot;href&quot;);
  if (!href) return;
  const url = new URL(href, window.location.origin);
  let hrefPath = url.pathname.toLowerCase();
  hrefPath = hrefPath.replace(/\/+$/, &quot;&quot;);
  if (hrefPath === &quot;&quot;) hrefPath = &quot;/&quot;;

  if (hrefPath === currentPath) {
    link.classList.add(&quot;active&quot;);
  }
});</code></pre>
<h2 class="wp-block-heading">為何要使用 URL 物件？</h2>
<p>使用 <code>new URL(href, window.location.origin)</code> 可以將相對路徑（例如 <code>../page</code>）轉換成完整的絕對路徑，避免直接比對相對路徑時因基底路徑不同而失準。這是實務中常見的路徑比對技巧。</p>
<h2 class="wp-block-heading">實際應用場景</h2>
<ul>
<li>多層次導覽列或側邊欄，需自動標示當前頁面。</li>
<li>靜態網站或沒有後端模板渲染的情況下，前端動態控制導覽狀態。</li>
</ul>
<h2 class="wp-block-heading">延伸優化方向</h2>
<ul>
<li>可加入支援查詢字串（query string）或 hash 的比對。</li>
<li>支援部分路徑匹配，例如 <code>/blog</code> 下所有頁面都標示為 active。</li>
<li>將功能封裝成函式，方便重複使用。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>確保側邊欄連結的 href 屬性正確，避免空值或錯誤路徑。</li>
<li>不同瀏覽器對 URL 物件支援度良好，但在非常舊版本可能需 polyfill。</li>
<li>若網站有使用前端路由（如 SPA），此方法需配合路由狀態更新。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&quot;DOMContentLoaded&quot;, function () {
    // 目前頁面的 path，例如 /investor-relations/corporate-governance/
    let currentPath = window.location.pathname.toLowerCase();
    // 移除結尾多餘的斜線（/a/b/ -&gt; /a/b）
    currentPath = currentPath.replace(/\/+$/, &quot;&quot;);
    if (currentPath === &quot;&quot;) currentPath = &quot;/&quot;;

    const links = document.querySelectorAll(&quot;.anchor-links .link&quot;);

    links.forEach(link =&gt; {
        const href = link.getAttribute(&quot;href&quot;);
        if (!href) return;

        // 轉成絕對網址再拿 pathname，避免相對路徑問題
        const url = new URL(href, window.location.origin);
        let hrefPath = url.pathname.toLowerCase();
        hrefPath = hrefPath.replace(/\/+$/, &quot;&quot;);
        if (hrefPath === &quot;&quot;) hrefPath = &quot;/&quot;;

        // ✅ 完整匹配才加 active
        if (hrefPath === currentPath) {
            link.classList.add(&quot;active&quot;);
        }
    });
});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/js-active-link-current-path/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>使用 JavaScript 實作動態切換 Tab 面板的按鈕控制</title>
		<link>https://piglife.tw/technical-notes/javascript-tab-panel-switch/</link>
					<comments>https://piglife.tw/technical-notes/javascript-tab-panel-switch/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 10:22:38 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[DOM操作]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Tab切換]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/javascript-tab-panel-switch/</guid>

					<description><![CDATA[介紹如何用純 JavaScript 實作按鈕控制的 Tab 面板切換功能，包含狀態管理與事件綁定，適...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在許多網頁應用中，常見的 UI 元件之一是 Tab 切換功能，讓使用者能在同一區塊內切換不同內容面板。這段程式碼示範如何用純 JavaScript 實作按鈕控制多個 Tab 面板的顯示與隱藏，適合有基礎 DOM 操作經驗，想了解如何手動管理元素狀態的前端工程師或自學者。</p>
<h2 class="wp-block-heading">按鈕與面板元素的選取</h2>
<p>首先，程式碼透過 <code>document.querySelectorAll</code> 分別取得所有具有 <code>.action-btn</code> 與 <code>.tab-panel</code> 類別的元素。這兩組元素分別代表切換按鈕與對應的內容面板。若任一組元素不存在，則直接結束執行，避免後續錯誤。</p>
<pre><code class="lang-javascript language-javascript javascript">const buttons = document.querySelectorAll(&#039;.action-btn&#039;);
const panels  = document.querySelectorAll(&#039;.tab-panel&#039;);
if (!buttons.length || !panels.length) return;</code></pre>
<h2 class="wp-block-heading">activateButton 函式設計</h2>
<p><code>activateButton</code> 是核心函式，負責切換按鈕的「活躍」狀態與對應面板的顯示。它透過按鈕的 <code>data-target</code> 屬性取得目標面板的類別名稱，並依序執行：</p>
<ol>
<li>移除所有按鈕的 <code>is-active</code> 樣式，確保只有一個按鈕處於活躍狀態。</li>
<li>為當前按鈕加上 <code>is-active</code> 樣式。</li>
<li>隱藏所有面板（移除 <code>is-active</code> 樣式）。</li>
<li>顯示對應目標面板（可支援多個面板同時顯示），加上 <code>is-active</code> 樣式。</li>
</ol>
<p>這種設計讓按鈕與面板的關聯透過 CSS 類別靈活控制，方便樣式調整與擴充。</p>
<pre><code class="lang-javascript language-javascript javascript">function activateButton(btn) {
  const target = btn.dataset.target;
  if (!target) return;

  buttons.forEach(b =&gt; b.classList.remove(&#039;is-active&#039;));
  btn.classList.add(&#039;is-active&#039;);

  panels.forEach(p =&gt; p.classList.remove(&#039;is-active&#039;));

  const targetPanels = document.querySelectorAll(&#039;.&#039; + target);
  targetPanels.forEach(p =&gt; p.classList.add(&#039;is-active&#039;));
}</code></pre>
<h2 class="wp-block-heading">綁定事件與初始化</h2>
<p>接著，為每個按鈕綁定點擊事件，點擊時呼叫 <code>activateButton</code>，並阻止預設行為（例如連結跳轉）。</p>
<p>最後，為了讓頁面載入時即有預設顯示的面板，程式碼自動觸發第一個按鈕的點擊事件，確保初始化狀態與使用者互動完全一致，避免手動設定狀態可能導致的不同步問題。</p>
<pre><code class="lang-javascript language-javascript javascript">buttons.forEach(btn =&gt; {
  btn.addEventListener(&#039;click&#039;, e =&gt; {
    e.preventDefault();
    activateButton(btn);
  });
});

buttons[0].click();</code></pre>
<h2 class="wp-block-heading">實務應用與延伸</h2>
<p>這種純前端的 Tab 切換實作適合用於靜態頁面或不依賴框架的專案。若需要支援動態面板內容或更複雜的狀態管理，可考慮結合前端框架或狀態管理工具。</p>
<p>此外，為提升無障礙性，可在按鈕加入 ARIA 屬性，並確保鍵盤操作友好。</p>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>確保每個按鈕的 <code>data-target</code> 對應的面板類別名稱正確，否則無法正確顯示。</li>
<li>若有多個面板共用同一類別，會同時顯示，這是設計上的彈性，但需注意樣式與結構。</li>
<li>自動觸發點擊事件的做法雖方便，但若未考慮瀏覽器支援或其他腳本，可能會有兼容性問題。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&#039;DOMContentLoaded&#039;, function () {
  const buttons = document.querySelectorAll(&#039;.action-btn&#039;);
  const panels  = document.querySelectorAll(&#039;.tab-panel&#039;);

  if (!buttons.length || !panels.length) return;

  function activateButton(btn) {
    const target = btn.dataset.target;
    if (!target) return;

    buttons.forEach(function (b) {
      b.classList.remove(&#039;is-active&#039;);
    });

    btn.classList.add(&#039;is-active&#039;);

    panels.forEach(function (p) {
      p.classList.remove(&#039;is-active&#039;);
    });

    const targetPanels = document.querySelectorAll(&#039;.&#039; + target);
    targetPanels.forEach(function (p) {
      p.classList.add(&#039;is-active&#039;);
    });
  }

  buttons.forEach(function (btn) {
    btn.addEventListener(&#039;click&#039;, function (e) {
      e.preventDefault();
      activateButton(btn);
    });
  });

  buttons[0].click();
});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/javascript-tab-panel-switch/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>使用 JavaScript 控制表格列依年份篩選顯示</title>
		<link>https://piglife.tw/technical-notes/javascript-table-row-filter/</link>
					<comments>https://piglife.tw/technical-notes/javascript-table-row-filter/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 10:22:08 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[DOM操作]]></category>
		<category><![CDATA[JavaScript]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/javascript-table-row-filter/</guid>

					<description><![CDATA[這篇文章說明如何使用純 JavaScript 根據下拉選單選擇，動態篩選並顯示表格中帶有年份標記的列...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在許多資料表格中，我們常需要根據某個條件（例如年份）來篩選顯示特定的列。這段程式碼示範如何利用純 JavaScript 操作 DOM，根據下拉選單的選擇動態控制表格中帶有特定屬性（data-block）的列顯示與隱藏。適合需要簡潔且無依賴外部函式庫的前端工程師或自學者參考。</p>
<h2 class="wp-block-heading">取得目標元素與資料列</h2>
<p>首先，我們透過 <code>document.getElementById</code> 取得下拉選單元素，並使用 <code>document.querySelectorAll</code> 選取所有帶有 <code>data-block</code> 屬性的表格列（tr）。這些列代表不同年份的資料區塊。</p>
<pre><code class="lang-javascript language-javascript javascript">const select = document.getElementById(&#039;select-download&#039;);
const blocks = Array.from(document.querySelectorAll(&#039;table tr[data-block]&#039;));</code></pre>
<h2 class="wp-block-heading">儲存原始顯示狀態</h2>
<p>為了避免直接覆寫 <code>display</code> 屬性導致無法還原，我們先讀取每個列的原始顯示狀態，並存入自訂的 <code>data-orig-display</code> 屬性中。這樣在切換篩選條件時，可以回復到原本的顯示方式。</p>
<pre><code class="lang-javascript language-javascript javascript">blocks.forEach(el =&gt; {
  if (!el.dataset.origDisplay) {
    const d = window.getComputedStyle(el).display;
    el.dataset.origDisplay = (d &amp;&amp; d !== &#039;none&#039;) ? d : &#039;block&#039;;
  }
});</code></pre>
<h2 class="wp-block-heading">篩選函式設計</h2>
<p><code>applyFilter</code> 函式會根據下拉選單的值，決定每個資料列是否顯示。若選擇 &#8220;all&#8221;，則全部顯示；否則只顯示 <code>data-block</code> 屬性值符合的列，其他則隱藏。</p>
<pre><code class="lang-javascript language-javascript javascript">function applyFilter() {
  const val = select.value; // 例如 &#039;all&#039; 或 &#039;2025&#039;
  blocks.forEach(el =&gt; {
    const year = el.getAttribute(&#039;data-block&#039;);
    if (val === &#039;all&#039; || year === val) {
      el.style.display = el.dataset.origDisplay;
    } else {
      el.style.display = &#039;none&#039;;
    }
  });
}</code></pre>
<h2 class="wp-block-heading">綁定事件與初始化</h2>
<p>程式碼在 DOMContentLoaded 時執行，確保元素已存在。初次執行一次篩選，並監聽下拉選單的 <code>change</code> 事件，動態更新顯示狀態。</p>
<pre><code class="lang-javascript language-javascript javascript">applyFilter();
select.addEventListener(&#039;change&#039;, applyFilter);</code></pre>
<h2 class="wp-block-heading">實務應用與延伸</h2>
<p>這種用法適合靜態或小型資料表格的前端篩選，無需後端或複雜框架支援。若資料量大，可考慮分頁或虛擬滾動優化效能。未來也能擴充多條件篩選，或搭配 CSS 動畫增強使用者體驗。</p>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>確保 <code>data-block</code> 屬性值與下拉選單選項一致。</li>
<li>若原始顯示狀態非 <code>block</code>，此程式碼會自動保存並還原，避免顯示異常。</li>
<li>若表格結構改變，需確認選取器仍正確。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&#039;DOMContentLoaded&#039;, function () {
  const select = document.getElementById(&#039;select-download&#039;);
  if (!select) return;

  // 只控制 Greenshift 的區塊，且有 data-block（年份）
  const blocks = Array.from(
    document.querySelectorAll(&#039;table tr[data-block]&#039;)
  );

  // 記住原本的 display（以防原本不是 block）
  blocks.forEach(el =&gt; {
    if (!el.dataset.origDisplay) {
      const d = window.getComputedStyle(el).display;
      el.dataset.origDisplay = (d &amp;&amp; d !== &#039;none&#039;) ? d : &#039;block&#039;;
    }
  });

  function applyFilter() {
    const val = select.value; // &#039;all&#039; 或 &#039;2025&#039;...
    blocks.forEach(el =&gt; {
      const year = el.getAttribute(&#039;data-block&#039;);
      if (val === &#039;all&#039; || year === val) {
        el.style.display = el.dataset.origDisplay;
      } else {
        el.style.display = &#039;none&#039;;
      }
    });
  }

  // 初次執行一次（依目前選中的值）
  applyFilter();

  // 監聽選單變更
  select.addEventListener(&#039;change&#039;, applyFilter);
});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/javascript-table-row-filter/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>使用 JavaScript 自動隱藏無目錄的 toc-block 區塊</title>
		<link>https://piglife.tw/technical-notes/javascript-hide-empty-toc-block/</link>
					<comments>https://piglife.tw/technical-notes/javascript-hide-empty-toc-block/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 10:21:42 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[DOMContentLoaded]]></category>
		<category><![CDATA[DOM操作]]></category>
		<category><![CDATA[JavaScript]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/javascript-hide-empty-toc-block/</guid>

					<description><![CDATA[本文介紹如何使用 JavaScript 在頁面載入後自動隱藏沒有目錄內容的 toc-block，提升...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在網頁中，我們常會使用目錄區塊（toc-block）來幫助使用者快速瀏覽內容，但有時候目錄內容（gs-toc）可能因為資料不完整或條件不符而不存在，這時候空白的目錄區塊反而會影響頁面美觀與使用體驗。本文示範如何用簡單的 JavaScript 程式碼，在頁面載入完成後自動檢查並隱藏沒有目錄內容的 toc-block，適合前端工程師或自學者優化動態內容顯示。</p>
<h2 class="wp-block-heading">為什麼要隱藏空的 toc-block</h2>
<p>空的目錄區塊會佔據頁面空間，造成視覺上的空洞，甚至誤導使用者以為頁面資料不完整。透過程式自動判斷並隱藏，可以提升頁面整潔度與使用者體驗，尤其在內容經常變動或由後端動態產生時更為重要。</p>
<h2 class="wp-block-heading">主要程式碼說明</h2>
<p>以下是核心程式碼片段：</p>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&quot;DOMContentLoaded&quot;, function() {
  const tocBlocks = document.querySelectorAll(&quot;.toc-block&quot;);

  tocBlocks.forEach(block =&gt; {
    const hasToc = block.querySelector(&quot;.gs-toc&quot;);
    if (!hasToc) {
      block.style.display = &quot;none&quot;;
    }
  });
});</code></pre>
<ul>
<li>使用 <code>DOMContentLoaded</code> 事件確保 DOM 元素已經載入完畢，避免查詢不到元素。</li>
<li>利用 <code>document.querySelectorAll</code> 選取所有 <code>.toc-block</code> 元素，方便批次處理。</li>
<li>針對每個 toc-block，使用 <code>querySelector</code> 檢查是否含有 <code>.gs-toc</code> 子元素。</li>
<li>若找不到 <code>.gs-toc</code>，代表此目錄區塊沒有內容，將其 <code>display</code> 設為 <code>none</code>，達到隱藏效果。</li>
</ul>
<h2 class="wp-block-heading">實務應用與優化方向</h2>
<ul>
<li>此方法適用於靜態或動態產生的目錄區塊，確保頁面不會顯示空白區塊。</li>
<li>若目錄內容是透過 AJAX 載入，需將檢查邏輯放在資料載入完成後執行。</li>
<li>可搭配 CSS 動畫效果，讓隱藏過程更為平滑。</li>
<li>若頁面中 toc-block 數量龐大，考慮使用更有效率的 DOM 操作或節流機制。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>確認 <code>.gs-toc</code> 是目錄內容的正確 class 名稱，避免誤判。</li>
<li>若頁面中有多層目錄結構，需調整選擇器以符合需求。</li>
<li>使用 <code>display: none</code> 會完全移除元素佔用空間，若想保留佔位可改用其他 CSS 屬性。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&quot;DOMContentLoaded&quot;, function() {
  // 取得所有 toc-block
  const tocBlocks = document.querySelectorAll(&quot;.toc-block&quot;);

  tocBlocks.forEach(block =&gt; {
    // 檢查這個 toc-block 底下是否有 gs-toc
    const hasToc = block.querySelector(&quot;.gs-toc&quot;);

    // 如果沒有找到，則隱藏整個區塊
    if (!hasToc) {
      block.style.display = &quot;none&quot;;
    }
  });
});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/javascript-hide-empty-toc-block/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>為 WordPress 分類清單實作手風琴展開收合功能</title>
		<link>https://piglife.tw/technical-notes/wordpress-category-accordion/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-category-accordion/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 10:21:20 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[DOM操作]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-category-accordion/</guid>

					<description><![CDATA[這篇文章解析如何為 WordPress 分類清單加入手風琴展開收合功能，包含無障礙 ARIA 屬性設...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在 WordPress 網站中，分類清單常用於側邊欄或導覽列，當分類層級較多時，使用者體驗會因為清單過長而降低。這段程式碼的目標是為多層分類清單加入手風琴（Accordion）功能，使得使用者可以展開或收合子分類，提升導覽的清晰度與操作便利性。本文適合有基礎 JavaScript 與 DOM 操作經驗的前端工程師或自學者，想了解如何結合無障礙設計（ARIA）與動態 DOM 操作來優化分類清單。</p>
<h2 class="wp-block-heading">目標與使用情境</h2>
<p>這段程式碼主要解決以下問題：</p>
<ul>
<li>多層分類清單展開收合控制</li>
<li>一次只允許展開一個分類（手風琴嚴格模式）</li>
<li>兼顧無障礙操作，使用 ARIA 屬性提示狀態</li>
<li>保持原有連結功能不受影響</li>
</ul>
<p>適用於 WordPress 產生的分類清單（如 <code>.wp-block-categories</code> 或 <code>.wp-block-categories-list</code>），並且希望加強使用者操作體驗與無障礙支援。</p>
<h2 class="wp-block-heading">主要流程解析</h2>
<h3 class="wp-block-heading">1. 選取目標清單容器</h3>
<pre><code class="lang-javascript language-javascript javascript">const roots = Array.from(document.querySelectorAll(&#039;.wp-block-categories, .wp-block-categories-list&#039;));
if (!roots.length) return;</code></pre>
<p>這裡先找出所有符合條件的分類清單容器，若無則直接結束，避免不必要的後續操作。</p>
<h3 class="wp-block-heading">2. 設定手風琴模式</h3>
<pre><code class="lang-javascript language-javascript javascript">const singleOpen = true;</code></pre>
<p>設定是否一次只允許展開一個分類項目，<code>true</code> 表示嚴格手風琴模式。</p>
<h3 class="wp-block-heading">3. 標記並處理有子分類的項目</h3>
<pre><code class="lang-javascript language-javascript javascript">const parents = Array.from(root.querySelectorAll(&#039;li.cat-item &gt; ul.children&#039;))
  .map(ul =&gt; ul.parentElement)
  .filter(li =&gt; li &amp;&amp; li.matches(&#039;.cat-item&#039;));</code></pre>
<p>找出所有有子清單的分類項目（<code>li.cat-item</code> 中包含 <code>ul.children</code>），這些項目需要被加上展開收合功能。</p>
<h3 class="wp-block-heading">4. 加入切換按鈕與 ARIA 屬性</h3>
<pre><code class="lang-javascript language-javascript javascript">const toggleBtn = document.createElement(&#039;button&#039;);
toggleBtn.className = &#039;cat-toggle&#039;;
toggleBtn.type = &#039;button&#039;;
toggleBtn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);
toggleBtn.setAttribute(&#039;aria-controls&#039;, subId);
toggleBtn.setAttribute(&#039;title&#039;, &#039;展開/收合&#039;);</code></pre>
<p>為每個有子分類的 <code>li</code> 加入一個按鈕，用於切換展開狀態，同時透過 <code>aria-expanded</code> 與 <code>aria-controls</code> 屬性提升無障礙體驗。</p>
<h3 class="wp-block-heading">5. 切換展開收合狀態的邏輯</h3>
<pre><code class="lang-javascript language-javascript javascript">function toggle(open) {
  const willOpen = (typeof open === &#039;boolean&#039;) ? open : !li.classList.contains(&#039;is-open&#039;);
  if (willOpen) {
    if (singleOpen) {
      const siblings = Array.from(li.parentElement.children)
        .filter(el =&gt; el !== li &amp;&amp; el.classList &amp;&amp; el.classList.contains(&#039;is-open&#039;));
      siblings.forEach(sib =&gt; {
        sib.classList.remove(&#039;is-open&#039;);
        const btn = sib.querySelector(&#039;:scope &gt; .cat-toggle&#039;);
        if (btn) btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);
      });
    }
    li.classList.add(&#039;is-open&#039;);
    toggleBtn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);
  } else {
    li.classList.remove(&#039;is-open&#039;);
    toggleBtn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);
  }
}</code></pre>
<p>此函式負責切換展開狀態，若啟用嚴格手風琴模式，會同時關閉同層其他已展開的分類。狀態透過 <code>is-open</code> 類別與按鈕的 <code>aria-expanded</code> 屬性同步更新。</p>
<h3 class="wp-block-heading">6. 綁定事件與點擊行為控制</h3>
<ul>
<li>按鈕點擊時切換展開狀態</li>
<li>點擊非連結與非按鈕的 <code>li</code> 項目空白處也能切換展開，避免影響原本連結導頁</li>
</ul>
<pre><code class="lang-javascript language-javascript javascript">toggleBtn.addEventListener(&#039;click&#039;, function(e) {
  e.preventDefault();
  e.stopPropagation();
  toggle();
});

li.addEventListener(&#039;click&#039;, function(e) {
  const isLink = e.target.closest(&#039;a&#039;);
  const isBtn  = e.target.closest(&#039;.cat-toggle&#039;);
  if (!isLink &amp;&amp; !isBtn) {
    e.preventDefault();
    toggle();
  }
});</code></pre>
<h3 class="wp-block-heading">7. 預設展開目前分類項目</h3>
<p>若分類項目本身或其子分類包含 <code>current-cat</code> 或 <code>current-cat-parent</code> 類別，則預設展開，方便使用者辨識目前所在分類。</p>
<pre><code class="lang-javascript language-javascript javascript">if (li.classList.contains(&#039;current-cat&#039;) ||
    li.classList.contains(&#039;current-cat-parent&#039;) ||
    sub.querySelector(&#039;.current-cat, .current-cat-parent&#039;)) {
  toggle(true);
}</code></pre>
<h2 class="wp-block-heading">實務應用與優化方向</h2>
<ul>
<li>可依需求調整 <code>singleOpen</code> 為 <code>false</code>，允許多個分類同時展開</li>
<li>按鈕樣式與動畫可加入 CSS 過渡效果，提升使用者體驗</li>
<li>可擴充鍵盤操作支援，增強無障礙友善度</li>
<li>若分類清單動態更新，需額外處理事件重新綁定</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>插入按鈕位置需謹慎，避免破壞原本的連結結構</li>
<li>使用 <code>aria-controls</code> 需確保對應的子清單有唯一 ID</li>
<li>點擊事件阻止預設行為，避免影響連結跳轉，需確認邏輯正確</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&#039;DOMContentLoaded&#039;, function() {
  // 你貼的清單容器 class
  const roots = Array.from(document.querySelectorAll(&#039;.wp-block-categories, .wp-block-categories-list&#039;));
  if (!roots.length) return;

  // 設定：是否一次只開一個（true = 手風琴嚴格模式）
  const singleOpen = true;

  roots.forEach(function(root) {
    // 標記啟用
    root.classList.add(&#039;cat-accordion&#039;);

    // 找到有 children 的 cat-item
    const parents = Array.from(root.querySelectorAll(&#039;li.cat-item &gt; ul.children&#039;))
      .map(ul =&gt; ul.parentElement)
      .filter(li =&gt; li &amp;&amp; li.matches(&#039;.cat-item&#039;));

    // 為每個需要的 li 補上 class 與切換按鈕/ARIA
    parents.forEach(function(li, idx) {
      li.classList.add(&#039;has-children&#039;);

      const sub = li.querySelector(&#039;:scope &gt; ul.children&#039;);
      // 建唯一 id，便於 aria-controls 連結
      const subId = sub.id || (&#039;cat-sub-&#039; + Math.random().toString(36).slice(2));
      sub.id = subId;

      // 建立切換按鈕（放在 
&lt;a&gt; 後面，不影響原有點擊導覽）
      const toggleBtn = document.createElement(&#039;button&#039;);
      toggleBtn.className = &#039;cat-toggle&#039;;
      toggleBtn.type = &#039;button&#039;;
      toggleBtn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);
      toggleBtn.setAttribute(&#039;aria-controls&#039;, subId);
      toggleBtn.setAttribute(&#039;title&#039;, &#039;展開/收合&#039;);

      // 插到 a 後（若沒有 a，則插到 li 內最前）
      const anchor = li.querySelector(&#039;:scope &gt; a&#039;);
      if (anchor &amp;&amp; anchor.nextSibling) {
        anchor.parentNode.insertBefore(toggleBtn, anchor.nextSibling);
      } else if (anchor) {
        anchor.parentNode.appendChild(toggleBtn);
      } else {
        li.insertBefore(toggleBtn, li.firstChild);
      }

      // 切換邏輯
      function toggle(open) {
        const willOpen = (typeof open === &#039;boolean&#039;) ? open : !li.classList.contains(&#039;is-open&#039;);
        if (willOpen) {
          if (singleOpen) {
            // 關閉同層其他
            const siblings = Array.from(li.parentElement.children)
              .filter(el =&gt; el !== li &amp;&amp; el.classList &amp;&amp; el.classList.contains(&#039;is-open&#039;));
            siblings.forEach(sib =&gt; {
              sib.classList.remove(&#039;is-open&#039;);
              const btn = sib.querySelector(&#039;:scope &gt; .cat-toggle&#039;);
              if (btn) btn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);
            });
          }
          li.classList.add(&#039;is-open&#039;);
          toggleBtn.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);
        } else {
          li.classList.remove(&#039;is-open&#039;);
          toggleBtn.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);
        }
      }

      // 綁定按鈕點擊
      toggleBtn.addEventListener(&#039;click&#039;, function(e) {
        e.preventDefault();
        e.stopPropagation();
        toggle();
      });

      // 如果想讓點 li 標題也能展開（但避免影響連結導頁）：
      // 只在點擊「li 空白處」才切換；點到 a 時仍正常導頁
      li.addEventListener(&#039;click&#039;, function(e) {
        const isLink = e.target.closest(&#039;a&#039;);
        const isBtn  = e.target.closest(&#039;.cat-toggle&#039;);
        if (!isLink &amp;&amp; !isBtn) {
          e.preventDefault();
          toggle();
        }
      });

      // 進階：如果此項或其子層有 &quot;current-cat&quot; / &quot;current-cat-parent&quot; 類別，預設展開
      if (li.classList.contains(&#039;current-cat&#039;) ||
          li.classList.contains(&#039;current-cat-parent&#039;) ||
          sub.querySelector(&#039;.current-cat, .current-cat-parent&#039;)) {
        toggle(true);
      }
    });
  });
});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-category-accordion/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>用 JavaScript 實作圖片縮圖點擊切換與自動輪播功能</title>
		<link>https://piglife.tw/technical-notes/javascript-thumbnail-autoplay-switch/</link>
					<comments>https://piglife.tw/technical-notes/javascript-thumbnail-autoplay-switch/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 10:19:51 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[JavaScript]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/javascript-thumbnail-autoplay-switch/</guid>

					<description><![CDATA[本文介紹如何用純 JavaScript 實作圖片縮圖點擊切換與自動輪播功能，詳解程式設計邏輯與實務應...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>這段程式碼解決了圖片縮圖點擊切換主圖與自動輪播的需求，適合需要在產品展示或相簿頁面實作圖片瀏覽功能的前端工程師與自學者。透過純 JavaScript 控制圖片切換與輪播，避免依賴第三方套件，方便客製化與維護。</p>
<h2 class="wp-block-heading">主要功能說明</h2>
<h3 class="wp-block-heading">取得縮圖與主圖元素</h3>
<p>程式碼一開始透過 <code>document.querySelectorAll</code> 抓取所有縮圖元素 <code>.gs-thumb</code>，以及主圖容器 <code>.gs-main-image</code>，確保兩者存在後才繼續執行。這樣避免因為元素不存在導致錯誤。</p>
<pre><code class="lang-javascript language-javascript javascript">const thumbs = Array.from(document.querySelectorAll(&#039;.gs-thumb&#039;));
const mainPicture = document.querySelector(&#039;.gs-main-image&#039;);
if (!thumbs.length || !mainPicture) return;</code></pre>
<h3 class="wp-block-heading">初始化目前索引</h3>
<p>先尋找哪個縮圖有 <code>.active</code> 樣式，代表當前顯示的圖片索引，若無則預設為 0。這樣設計方便在 HTML 中預先設定初始狀態。</p>
<pre><code class="lang-javascript language-javascript javascript">let currentIndex = thumbs.findIndex(t =&gt; t.classList.contains(&#039;active&#039;));
if (currentIndex === -1) currentIndex = 0;</code></pre>
<h3 class="wp-block-heading">切換主圖與縮圖的 active 樣式</h3>
<p><code>showThumb</code> 函式負責切換主圖與縮圖的狀態。它會從指定索引的縮圖抓取圖片路徑與 WebP 格式的 <code>srcset</code>，更新主圖的 <code>&lt;img&gt;</code> 與 <code>&lt;source&gt;</code> 標籤。</p>
<p>為了增加過場動畫效果，對主圖 <code>&lt;img&gt;</code> 加上 <code>switching</code> class，150 毫秒後移除，讓 CSS 可以控制淡入淡出等動畫。</p>
<p>最後將所有縮圖的 <code>.active</code> 移除，並將目前的縮圖加上 <code>.active</code>，同步視覺狀態。</p>
<pre><code class="lang-javascript language-javascript javascript">function showThumb(index) {
  const picture = thumbs[index];
  if (!picture) return;

  const thumbImg = picture.querySelector(&#039;img&#039;);
  if (!thumbImg) return;

  const newSrc = thumbImg.getAttribute(&#039;src&#039;);
  const newWebp = picture.querySelector(&#039;source&#039;)?.getAttribute(&#039;srcset&#039;) || &#039;&#039;;

  if (mainSource &amp;&amp; newWebp) {
    mainSource.setAttribute(&#039;srcset&#039;, newWebp);
  }

  if (mainImg &amp;&amp; newSrc) {
    mainImg.classList.add(&#039;switching&#039;);
    mainImg.setAttribute(&#039;src&#039;, newSrc);
    setTimeout(() =&gt; {
      mainImg.classList.remove(&#039;switching&#039;);
    }, 150);
  }

  thumbs.forEach(el =&gt; el.classList.remove(&#039;active&#039;));
  picture.classList.add(&#039;active&#039;);

  currentIndex = index;
}</code></pre>
<h3 class="wp-block-heading">自動輪播機制</h3>
<p><code>startAutoplay</code> 使用 <code>setInterval</code> 每 3 秒自動切換到下一張縮圖，並呼叫 <code>showThumb</code> 更新主圖。索引會循環回到第一張，確保輪播無限循環。</p>
<p><code>resetAutoplay</code> 則在使用者點擊縮圖時清除舊的計時器並重新啟動，避免自動輪播與手動操作衝突。</p>
<pre><code class="lang-javascript language-javascript javascript">function startAutoplay() {
  autoplayTimer = setInterval(() =&gt; {
    const nextIndex = (currentIndex + 1) % thumbs.length;
    showThumb(nextIndex);
  }, 3000);
}

function resetAutoplay() {
  if (autoplayTimer) clearInterval(autoplayTimer);
  startAutoplay();
}</code></pre>
<h3 class="wp-block-heading">點擊縮圖切換邏輯</h3>
<p>監聽整個文件的點擊事件，判斷點擊目標是否在 <code>.gs-thumb</code> 元素內，若是則取得該縮圖索引，呼叫 <code>showThumb</code> 切換主圖，並重置自動輪播計時。</p>
<p>這樣的事件代理方式有效減少監聽器數量，也方便動態新增縮圖時仍能正常運作。</p>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&#039;click&#039;, function (e) {
  const picture = e.target.closest(&#039;.gs-thumb&#039;);
  if (!picture) return;

  const index = thumbs.indexOf(picture);
  if (index === -1) return;

  showThumb(index);
  resetAutoplay();
});</code></pre>
<h2 class="wp-block-heading">實務應用與優化方向</h2>
<ul>
<li>可結合 CSS 進行更豐富的過場動畫，如淡入淡出、縮放等。</li>
<li>若圖片數量龐大，考慮加入懶加載以提升效能。</li>
<li>自動輪播時間可改為參數化，讓使用者自訂。</li>
<li>可增加鍵盤左右鍵切換支援，提升無障礙。</li>
<li>若需支援觸控裝置，加入滑動手勢切換功能。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-javascript language-javascript javascript">document.addEventListener(&#039;DOMContentLoaded&#039;, function () {
  const thumbs = Array.from(document.querySelectorAll(&#039;.gs-thumb&#039;));
  const mainPicture = document.querySelector(&#039;.gs-main-image&#039;);
  if (!thumbs.length || !mainPicture) return;

  const mainImg = mainPicture.querySelector(&#039;img&#039;);
  const mainSource = mainPicture.querySelector(&#039;source&#039;);

  let currentIndex = thumbs.findIndex(t =&gt; t.classList.contains(&#039;active&#039;));
  if (currentIndex === -1) currentIndex = 0;

  let autoplayTimer = null;

  function showThumb(index) {
    const picture = thumbs[index];
    if (!picture) return;

    const thumbImg = picture.querySelector(&#039;img&#039;);
    if (!thumbImg) return;

    const newSrc = thumbImg.getAttribute(&#039;src&#039;);
    const newWebp = picture.querySelector(&#039;source&#039;)?.getAttribute(&#039;srcset&#039;) || &#039;&#039;;

    if (mainSource &amp;&amp; newWebp) {
      mainSource.setAttribute(&#039;srcset&#039;, newWebp);
    }

    if (mainImg &amp;&amp; newSrc) {
      mainImg.classList.add(&#039;switching&#039;);
      mainImg.setAttribute(&#039;src&#039;, newSrc);
      setTimeout(() =&gt; {
        mainImg.classList.remove(&#039;switching&#039;);
      }, 150);
    }

    thumbs.forEach(el =&gt; el.classList.remove(&#039;active&#039;));
    picture.classList.add(&#039;active&#039;);

    currentIndex = index;
  }

  function startAutoplay() {
    autoplayTimer = setInterval(() =&gt; {
      const nextIndex = (currentIndex + 1) % thumbs.length;
      showThumb(nextIndex);
    }, 3000);
  }

  function resetAutoplay() {
    if (autoplayTimer) clearInterval(autoplayTimer);
    startAutoplay();
  }

  document.addEventListener(&#039;click&#039;, function (e) {
    const picture = e.target.closest(&#039;.gs-thumb&#039;);
    if (!picture) return;

    const index = thumbs.indexOf(picture);
    if (index === -1) return;

    showThumb(index);
    resetAutoplay();
  });

  showThumb(currentIndex);
  startAutoplay();
});</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/javascript-thumbnail-autoplay-switch/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WordPress 單篇文章中為程式碼區塊加入右上角複製按鈕</title>
		<link>https://piglife.tw/technical-notes/wordpress-code-copy-button/</link>
					<comments>https://piglife.tw/technical-notes/wordpress-code-copy-button/#respond</comments>
		
		<dc:creator><![CDATA[小豬]]></dc:creator>
		<pubDate>Wed, 10 Dec 2025 09:51:40 +0000</pubDate>
				<category><![CDATA[技術筆記]]></category>
		<category><![CDATA[Clipboard API]]></category>
		<category><![CDATA[Gutenberg]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[WordPress]]></category>
		<guid isPermaLink="false">https://piglife.tw/technical-notes/wordpress-code-copy-button/</guid>

					<description><![CDATA[本文介紹如何在 WordPress 單篇文章頁面，為所有程式碼區塊動態加入右上角複製按鈕，提升讀者複...]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">前言</h2>
<p>在撰寫技術文章時，常會使用程式碼區塊展示範例程式碼。為了提升讀者體驗，讓他們能快速複製程式碼，本文示範如何在 WordPress 單篇文章頁面中，為所有  pre , code 程式碼區塊動態加入一個右上角的「Copy」按鈕。此功能適用於 Gutenberg、Markdown、Prism.js 或 Highlight.js 等常見的程式碼高亮方案。</p>
<p>此做法適合熟悉 WordPress 主題或外掛開發的工程師，想要自訂前端行為並強化內容互動性。</p>
<h2 class="wp-block-heading">為什麼要這樣設計？</h2>
<p>透過 WordPress 的 <code>wp_footer</code> action，我們能在頁面底部注入必要的 CSS 與 JavaScript，確保只在前台且單篇文章頁面執行，避免管理後台或非文章頁面產生不必要的負擔。</p>
<p>此外，使用 JavaScript 動態包裝 pre 元素並加入按鈕，能兼容多種程式碼呈現方式，且避免重複包裝，確保效能與穩定性。</p>
<h2 class="wp-block-heading">核心程式碼解析</h2>
<h3 class="wp-block-heading">1. 僅在單篇文章頁面執行</h3>
<pre><code class="lang-php language-php php">if ( ! is_singular( &#039;post&#039; ) || is_admin() ) {
    return;
}</code></pre>
<p>這段檢查確保腳本只在前台的單篇文章頁面執行，避免影響其他頁面或後台。</p>
<h3 class="wp-block-heading">2. CSS 樣式定義</h3>
<pre><code class="lang-css language-css css">.code-copy-btn {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 10;
    padding: 4px 10px;
    font-size: 12px;
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.65);
    color: #fff;
    cursor: pointer;
    opacity: 0.75;
    transition: background 0.2s ease, opacity 0.2s ease;
}
.code-copy-btn:hover {
    background: rgba(0, 0, 0, 0.85);
    opacity: 1;
}
.code-copy-btn:disabled {
    cursor: default;
    opacity: 0.6;
}</code></pre>
<p>這些樣式將按鈕定位在程式碼區塊右上角，並提供滑鼠懸停與禁用狀態的視覺反饋。</p>
<h3 class="wp-block-heading">3. 動態包裝與按鈕建立</h3>
<pre><code class="lang-js language-js js">document.querySelectorAll(&#039;pre &gt; code&#039;).forEach(function (codeBlock) {
    const pre = codeBlock.parentNode;
    if (pre.parentNode.classList.contains(&#039;code-block-wrapper&#039;)) return;

    const wrapper = document.createElement(&#039;div&#039;);
    wrapper.className = &#039;code-block-wrapper&#039;;
    pre.parentNode.insertBefore(wrapper, pre);
    wrapper.appendChild(pre);

    const btn = document.createElement(&#039;button&#039;);
    btn.type = &#039;button&#039;;
    btn.className = &#039;code-copy-btn&#039;;
    btn.textContent = &#039;Copy&#039;;

    btn.addEventListener(&#039;click&#039;, function () {
        const text = codeBlock.innerText;
        navigator.clipboard.writeText(text).then(function () {
            btn.textContent = &#039;Copied!&#039;;
            btn.disabled = true;
            setTimeout(function () {
                btn.textContent = &#039;Copy&#039;;
                btn.disabled = false;
            }, 1500);
        });
    });

    wrapper.appendChild(btn);
});</code></pre>
<p>這段程式碼會搜尋所有 pre code 結構，為每個程式碼區塊包裹一個容器，並在右上角加入按鈕。按鈕點擊時會將程式碼文字複製到剪貼簿，並短暫顯示 &#8220;Copied!&#8221; 提示。</p>
<h2 class="wp-block-heading">實務應用與優化建議</h2>
<ul>
<li>可依需求調整按鈕文字或樣式，讓 UI 更符合網站風格。</li>
<li>若網站使用 AJAX 載入文章內容，需確保此腳本在新內容載入後重新執行。</li>
<li>為提升相容性，可加入對 <code>navigator.clipboard</code> 支援性的檢查，並提供替代方案。</li>
<li>若程式碼區塊非常長，複製整段可能影響使用者體驗，可考慮限制複製範圍或加入分段複製功能。</li>
</ul>
<h2 class="wp-block-heading">常見問題與注意事項</h2>
<ul>
<li>本方案依賴現代瀏覽器的 Clipboard API，舊版瀏覽器可能無法正常複製。</li>
<li>若網站有其他 JavaScript 操作程式碼區塊，請注意避免衝突。</li>
<li>按鈕的絕對定位需確保父容器有相對定位，否則位置會錯亂。</li>
</ul>
<h2 class="wp-block-heading">完整程式碼</h2>
<pre><code class="lang-php language-php php">&lt;?php
/**
 * 單篇 post 頁面：全站 
&lt;pre&gt;&lt;code&gt; 右上角加入 Copy 按鈕
 * 適用 Gutenberg / Markdown / Prism / Highlight.js
 */

add_action( &#039;wp_footer&#039;, function () {

    // ✅ 只在前台「單篇文章（post）」執行
    if ( ! is_singular( &#039;post&#039; ) || is_admin() ) {
        return;
    }
    ?&gt;

&lt;style&gt;
        .code-block-wrapper {
            position: relative;
        }

        .code-copy-btn {
            position: absolute;
            top: 8px;
            right: 8px;
            z-index: 10;
            padding: 4px 10px;
            font-size: 12px;
            line-height: 1.4;
            border-radius: 6px;
            border: none;
            background: rgba(0, 0, 0, 0.65);
            color: #fff;
            cursor: pointer;
            transition: background 0.2s ease, opacity 0.2s ease;
            opacity: 0.75;
        }

        .code-copy-btn:hover {
            background: rgba(0, 0, 0, 0.85);
            opacity: 1;
        }

        .code-copy-btn:disabled {
            cursor: default;
            opacity: 0.6;
        }

        .code-block-wrapper pre {
            margin: 0;
        }
    &lt;/style&gt;

    &lt;script&gt;
    document.addEventListener(&#039;DOMContentLoaded&#039;, function () {

        document.querySelectorAll(&#039;pre &gt; code&#039;).forEach(function (codeBlock) {

            const pre = codeBlock.parentNode;

            // 已處理過就跳過（避免 AJAX / 重複包）
            if (pre.parentNode.classList.contains(&#039;code-block-wrapper&#039;)) {
                return;
            }

            // 建立包裝容器
            const wrapper = document.createElement(&#039;div&#039;);
            wrapper.className = &#039;code-block-wrapper&#039;;

            pre.parentNode.insertBefore(wrapper, pre);
            wrapper.appendChild(pre);

            // 建立 Copy 按鈕
            const btn = document.createElement(&#039;button&#039;);
            btn.type = &#039;button&#039;;
            btn.className = &#039;code-copy-btn&#039;;
            btn.textContent = &#039;Copy&#039;;

            btn.addEventListener(&#039;click&#039;, function () {
                const text = codeBlock.innerText;

                navigator.clipboard.writeText(text).then(function () {
                    btn.textContent = &#039;Copied!&#039;;
                    btn.disabled = true;

                    setTimeout(function () {
                        btn.textContent = &#039;Copy&#039;;
                        btn.disabled = false;
                    }, 1500);
                });
            });

            wrapper.appendChild(btn);
        });

    });
    &lt;/script&gt;
    &lt;?php
});
</code></pre>]]></content:encoded>
					
					<wfw:commentRss>https://piglife.tw/technical-notes/wordpress-code-copy-button/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
