diff --git a/assets/javascripts/app.js b/assets/javascripts/app.js index 33dd23c..76c5164 100644 --- a/assets/javascripts/app.js +++ b/assets/javascripts/app.js @@ -310,7 +310,13 @@ $('.mobile-menu').toggleClass('active'); $('body').toggleClass('noscroll'); - $('.mobile-menu .navbar-toggle').removeClass('collapsed'); + + // ★ 修正:依據 active 狀態決定 collapsed 的加/移除 + if ($('.mobile-menu').hasClass('active')) { + $('.mobile-menu .navbar-toggle').removeClass('collapsed'); // 打開 → 叉叉 + } else { + $('.mobile-menu .navbar-toggle').addClass('collapsed'); // 關閉 → 三條橫線 + } // 關閉所有下拉選單 $('.mobile-menu1 > ul, .mobile-menu2 > ul').slideUp(500); @@ -324,6 +330,34 @@ } }); + // ✅ Tab 鍵跳過 .mobile-menu 時,觸發收合邏輯 + $(document).on('focusout', '.mobile-menu', function(e) { + setTimeout(function() { + var $newFocus = $(document.activeElement); + + // 焦點在 body 時代表滑鼠正在點擊中,不處理 + if ($newFocus.is('body')) return; + + if ($('.mobile-menu').hasClass('active') && $newFocus.closest('.mobile-menu').length === 0) { + + var $btn = $('.navbar-toggle').first(); + + if ($btn.attr('data-bs-toggle')) { + var target = $btn.attr('data-bs-target'); + var bsCollapse = window.bootstrap && bootstrap.Collapse.getInstance($(target)[0]); + if (bsCollapse) bsCollapse.hide(); + $('.mobile-menu').removeClass('active'); + $('body').removeClass('noscroll'); + $('.mobile-menu .navbar-toggle').addClass('collapsed'); + $('.mobile-menu1 > ul, .mobile-menu2 > ul').slideUp(500); + $('.mobile-menu1 > .menu-drop, .mobile-menu2 > .menu-drop').removeClass('opened'); + } else { + $btn.trigger('click'); + } + } + }, 50); + }); + $('.mobile-menu1 > .menu-drop').click(function(){ var $that = $(this); var opencheck1 = $that.hasClass('opened'); @@ -966,284 +1000,372 @@ setTimeout(fixImageAccessibility, 500); }); - // 有連結目的之所有a標籤的無障礙處理 - $(function() { - /** - * 全站無障礙 AA 級補強 (整合 HM1240400C 與 HM1240401C 規範) - */ - function fixAccessibilityAll() { - // --- 0. 環境與語言偵測 --- - //1200ms 後執行(確保所有其他 function 執行完畢後才處理) - //Part 1:修正語言選單按鈕 - //Part 2:掃描所有 - //├─ 排除 navbar-brand、javascript: 連結 - //├─ 空連結 → 隱藏 - //├─ 文字是路徑/空 → 補隱藏 span - //├─ 圖片+文字共存 → 圖片 alt 清空 - //├─ 無 title → 自動產出 title - //├─ 有 title 但與連結文字重複 → 只保留開窗提示 - //├─ 有 title 出現空括號 () → 清除並補上正確開窗提示 - //└─ 移除 aria-label - var urlParams = new URLSearchParams(window.location.search); - var isEditMode = urlParams.get('editmode') === 'on'; +// 有連結目的之所有a標籤的無障礙處理 +$(function() { + // Bootstrap 會把原始 title 搬到 data-bs-original-title,先還原回來 + $('a[data-bs-original-title]').each(function() { + var $a = $(this); + var originalTitle = $a.attr('data-bs-original-title'); + if (originalTitle && originalTitle.trim() !== '') { + $a.attr('title', originalTitle); + } + }); - var pageLang = $('html').attr('lang') || 'zh-Hant'; - var isEn = pageLang.toLowerCase().startsWith('en'); + /** + * 全站無障礙 AA 級補強 (整合 HM1240400C 與 HM1240401C 規範) + */ + function fixAccessibilityAll() { + // --- 0. 環境與語言偵測 --- + //1200ms 後執行(確保所有其他 function 執行完畢後才處理) + //Part 1:修正語言選單按鈕 + //Part 2:掃描所有 + //├─ 排除 navbar-brand、javascript: 連結 + //├─ 空連結 → 隱藏 + //├─ 文字是路徑/空 → 補隱藏 span + //├─ 圖片+文字共存 → 圖片 alt 清空 + //├─ 無 title → 自動產出 title + //├─ 有 title 但與連結文字或圖片 alt 重複 → 只保留開窗提示 + //├─ 有 title 出現空括號 () → 清除並補上正確開窗提示 + //└─ 移除 aria-label + var urlParams = new URLSearchParams(window.location.search); + var isEditMode = urlParams.get('editmode') === 'on'; - var i18n = { - langBtn: isEn ? 'Open language menu in this window' : '在本視窗開啟語言選單', - newWin: isEn ? 'Open in new window' : '在新視窗開啟', - selfWin: isEn ? 'Open in this window' : '在本視窗開啟', - link: isEn ? 'Link' : '連結', - openImg: isEn ? 'Open image' : '開啟圖片內容', - sort: isEn ? 'Sort' : '排序' - }; + var pageLang = $('html').attr('lang') || 'zh-Hant'; + var isEn = pageLang.toLowerCase().startsWith('en'); - // --- Part 1: 語言選單修正 --- - var $langBtn = $('#languagebutton'); - if ($langBtn.length > 0) { - $langBtn.attr('title', i18n.langBtn); - $langBtn.removeAttr('aria-label'); + var i18n = { + langBtn: isEn ? 'Open language menu in this window' : '在本視窗開啟語言選單', + newWin: isEn ? 'Open in new window' : '在新視窗開啟', + selfWin: isEn ? 'Open in this window' : '在本視窗開啟', + link: isEn ? 'Link' : '連結', + openImg: isEn ? 'Open image' : '開啟圖片內容', + sort: isEn ? 'Sort' : '排序' + }; + + // --- Part 1: 語言選單修正 --- + var $langBtn = $('#languagebutton'); + if ($langBtn.length > 0) { + $langBtn.attr('title', i18n.langBtn); + $langBtn.removeAttr('aria-label'); + } + + // --- Part 2: 全站連結與圖片邏輯處理 --- + $('a').each(function() { + var $a = $(this); + + // 【條件 1:排除特定 class】 + if ($a.hasClass('navbar-brand')) { + return; } - // --- Part 2: 全站連結與圖片邏輯處理 --- - $('a').each(function() { - var $a = $(this); + var href = ($a.attr('href') || '').toLowerCase(); + if (!href || href.startsWith('javascript:')) return; - // 【條件 1:排除特定 class】 - if ($a.hasClass('navbar-brand')) { - return; + // --- 關鍵修正:先偵測是否有原始 title --- + var rawTitleAttr = $a.attr('title'); + var hasOriginalTitle = (typeof rawTitleAttr !== 'undefined' && rawTitleAttr !== false && rawTitleAttr.trim() !== ""); + + // 清理 與 空白 + var linkText = $a.text().replace(/\u00a0/g, '').trim(); + var $img = $a.find('img'); + var $icons = $a.find('i, svg'); + var currentTitle = ($a.attr('title') || '').trim(); + + // ✅ 在 D 區塊清空 alt 之前,先記錄圖片原始 alt + var imgAltOriginal = $img.length > 0 ? ($img.attr('alt') || '').trim() : ''; + + // --- 【修正 1:隱藏空連結】 --- + if (!isEditMode && linkText === "" && $img.length === 0 && $icons.length === 0) { + $a.hide(); + return; + } + + // --- 【修正 2:文字語意防禦】 --- + if ((linkText === "" || linkText.startsWith('/') || linkText.includes('?')) && $img.length === 0 && $icons.length === 0) { + var accessibleText = i18n.link; + if (href.includes('sort')) { + accessibleText = i18n.sort; } - - var href = ($a.attr('href') || '').toLowerCase(); - if (!href || href.startsWith('javascript:')) return; - - // --- 關鍵修正:先偵測是否有原始 title --- - var rawTitleAttr = $a.attr('title'); - var hasOriginalTitle = (typeof rawTitleAttr !== 'undefined' && rawTitleAttr !== false && rawTitleAttr.trim() !== ""); - - // 清理 與 空白 - var linkText = $a.text().replace(/\u00a0/g, '').trim(); - var $img = $a.find('img'); - var $icons = $a.find('i, svg'); - var currentTitle = ($a.attr('title') || '').trim(); - // --- 【修正 1:隱藏空連結】 --- - if (!isEditMode && linkText === "" && $img.length === 0 && $icons.length === 0) { - $a.hide(); - return; + var $span = $a.find('span'); + if ($span.length > 0) { + $span.text(accessibleText); + } else { + $a.append('' + accessibleText + ''); + $span = $a.find('span').last(); } - // --- 【修正 2:文字語意防禦】 --- - if ((linkText === "" || linkText.startsWith('/') || linkText.includes('?')) && $img.length === 0) { - var accessibleText = i18n.link; - if (href.includes('sort')) { - accessibleText = i18n.sort; - } - - var $span = $a.find('span'); - if ($span.length > 0) { - $span.text(accessibleText); - } else { - $a.append('' + accessibleText + ''); - $span = $a.find('span').last(); - } + $span.css({ + 'display': 'inline-block', 'position': 'absolute', 'width': '1px', 'height': '1px', + 'padding': '0', 'margin': '-1px', 'overflow': 'hidden', 'clip': 'rect(0, 0, 0, 0)', + 'white-space': 'nowrap', 'border': '0' + }); + linkText = accessibleText; + } - $span.css({ - 'display': 'inline-block', 'position': 'absolute', 'width': '1px', 'height': '1px', - 'padding': '0', 'margin': '-1px', 'overflow': 'hidden', 'clip': 'rect(0, 0, 0, 0)', - 'white-space': 'nowrap', 'border': '0' - }); - linkText = accessibleText; + // A. 視窗動作規範 + var isNewWindow = ($a.attr('target') === '_blank'); + var windowTask = isNewWindow ? i18n.newWin : i18n.selfWin; + + // B. 偵測檔案類型 + var fileExt = ""; + var fileMatches = href.match(/\.(pdf|doc|docx|xls|xlsx|ppt|pptx|odt|ods|odp|zip|rar|jpg|png|csv)$/); + if (fileMatches) { + fileExt = fileMatches[1].toLowerCase(); + } + + // C. 智慧修正標題判斷 + var hasBadSymbol = currentTitle.includes('()') || + currentTitle.includes('Open in new window') || + currentTitle.includes('/uploads/'); + + if (currentTitle.startsWith('/') || currentTitle.includes('?')) { + currentTitle = ""; + } + + // D. 處理【HM1240400C】:圖片與文字毗鄰時,替代文字只能有一份 + if (($img.length > 0 || $icons.length > 0) && linkText !== "") { + if ($img.length > 0) { + $img.attr('alt', ''); } - // A. 視窗動作規範 - var isNewWindow = ($a.attr('target') === '_blank'); - var windowTask = isNewWindow ? i18n.newWin : i18n.selfWin; + var $hiddenSpans = $a.find('span').filter(function() { + return $(this).css('display') === 'none' || $(this).attr('style')?.includes('display: none'); + }); - // B. 偵測檔案類型 - var fileExt = ""; - var fileMatches = href.match(/\.(pdf|doc|docx|xls|xlsx|ppt|pptx|odt|ods|odp|zip|rar|jpg|png|csv)$/); - if (fileMatches) { - fileExt = fileMatches[1].toLowerCase(); - } - - // C. 智慧修正標題判斷 - var hasBadSymbol = currentTitle.includes('()') || - currentTitle.includes('Open in new window') || - currentTitle.includes('/uploads/'); - - if (currentTitle.startsWith('/') || currentTitle.includes('?')) { - currentTitle = ""; - } - - // D. 處理【HM1240400C】:圖片與文字毗鄰時,替代文字只能有一份 - if (($img.length > 0 || $icons.length > 0) && linkText !== "") { - if ($img.length > 0) { - $img.attr('alt', ''); - } - - var $hiddenSpans = $a.find('span').filter(function() { - return $(this).css('display') === 'none' || $(this).attr('style')?.includes('display: none'); - }); - - if ($hiddenSpans.length > 0) { - $hiddenSpans.each(function() { - var $span = $(this); - if ($span.text().includes('/uploads/')) { - $span.text(i18n.openImg); - } - - $span.css({ - 'display': 'inline-block', - 'position': 'absolute', - 'width': '1px', - 'height': '1px', - 'padding': '0', - 'margin': '-1px', - 'overflow': 'hidden', - 'clip': 'rect(0, 0, 0, 0)', - 'white-space': 'nowrap', - 'border': '0' - }); - }); - } - } - - // E. 重新計算標題 (關鍵邏輯:若 hasOriginalTitle 為真,則不進入此區塊) - if (!hasOriginalTitle) { - if (linkText === "" && $img.length > 0) { - var rawAlt = ($img.attr('alt') || '').trim(); - var forbidden = ['這是一張圖片', '圖片', 'image', 'photo', '圖', '']; - var imgAlt = forbidden.some(function(txt) { return rawAlt.toLowerCase() === txt; }) - ? ($a.attr('aria-label') || i18n.link) : rawAlt; + if ($hiddenSpans.length > 0) { + $hiddenSpans.each(function() { + var $span = $(this); + if ($span.text().includes('/uploads/')) { + $span.text(i18n.openImg); + } - var fileInfo = fileExt ? '[' + fileExt.toUpperCase() + '] ' : ''; - $a.attr('title', (fileInfo + imgAlt + " " + windowTask).trim()); - $img.attr('alt', imgAlt); - } - else if (linkText !== "") { - if (fileExt !== "" || hasBadSymbol || currentTitle === "" || currentTitle.includes('/uploads/')) { - if (fileExt !== "") { - $a.attr('title', linkText + " ." + fileExt + " " + windowTask); - } else { - $a.attr('title', windowTask); - } + $span.css({ + 'display': 'inline-block', + 'position': 'absolute', + 'width': '1px', + 'height': '1px', + 'padding': '0', + 'margin': '-1px', + 'overflow': 'hidden', + 'clip': 'rect(0, 0, 0, 0)', + 'white-space': 'nowrap', + 'border': '0' + }); + }); + } + } + + // E. 重新計算標題 (關鍵邏輯:若 hasOriginalTitle 為真,則不進入此區塊) + if (!hasOriginalTitle) { + if (linkText === "" && $img.length > 0) { + var rawAlt = ($img.attr('alt') || '').trim(); + var forbidden = ['這是一張圖片', '圖片', 'image', 'photo', '圖', '']; + var imgAlt = forbidden.some(function(txt) { return rawAlt.toLowerCase() === txt; }) + ? ($a.attr('aria-label') || i18n.link) : rawAlt; + + var fileInfo = fileExt ? '[' + fileExt.toUpperCase() + '] ' : ''; + $a.attr('title', (fileInfo + imgAlt + " " + windowTask).trim()); + $img.attr('alt', imgAlt); + } + else if (linkText !== "") { + if (fileExt !== "" || hasBadSymbol || currentTitle === "" || currentTitle.includes('/uploads/')) { + if (fileExt !== "") { + $a.attr('title', linkText + " ." + fileExt + " " + windowTask); + } else { + $a.attr('title', windowTask); } } } + } - // ✅ 符合 HM1240404E:title 不應與連結文字重複 - if (hasOriginalTitle && currentTitle !== "") { - var titleWithoutWindow = currentTitle - .replace('(在本視窗開啟)', '') - .replace('(在新視窗開啟)', '') - .replace('Open in this window', '') - .replace('Open in new window', '') - .replace('另開新視窗前往', '') - .replace('另開新視窗', '') - .trim(); + // ✅ 符合 HM1240404E:title 不應與連結文字或圖片 alt 重複 + if (hasOriginalTitle && currentTitle !== "") { + var titleWithoutWindow = currentTitle + .replace('(在本視窗開啟)', '') + .replace('(在新視窗開啟)', '') + .replace('Open in this window', '') + .replace('Open in new window', '') + .replace('另開新視窗前往', '') + .replace('另開新視窗', '') + .trim(); - if (titleWithoutWindow === linkText) { - $a.attr('title', windowTask); - } - - // ✅ 最終清理:若 title 出現空括號 () 則移除並補上正確開窗提示 - var finalTitle = ($a.attr('title') || '').trim(); - if (finalTitle.includes('()')) { - finalTitle = finalTitle.replace(/\(\)/g, '').trim(); - $a.attr('title', finalTitle + ' ' + windowTask); - } + // ✅ title 去掉開窗字串後,若與連結文字或圖片原始 alt 相同,則只保留開窗提示 + if ( + (titleWithoutWindow === linkText && linkText !== i18n.link) || + (imgAltOriginal !== '' && titleWithoutWindow === imgAltOriginal) + ) { + $a.attr('title', windowTask); } - // F. 最終清理:移除冗餘 label,避免螢幕閱讀器唸兩次 - $a.removeAttr('aria-label'); - }); - - // ✅ G. 補強特定連結(須在迴圈外,因為 javascript: 連結被迴圈跳過) - $('.btn-o a').each(function() { - var $a = $(this); - if (!$a.attr('target')) { - $a.attr('target', '_blank'); - $a.attr('rel', 'noopener noreferrer'); + // ✅ 最終清理:若 title 出現空括號 () 則移除並補上正確開窗提示 + var finalTitle = ($a.attr('title') || '').trim(); + if (finalTitle.includes('()')) { + finalTitle = finalTitle.replace(/\(\)/g, '').trim(); + $a.attr('title', finalTitle + ' ' + windowTask); } - $a.attr('title', isEn ? 'Share to X Open in new window' : '分享至 X 在新視窗開啟'); - }); + } - $('.print-button a').each(function() { - var $a = $(this); - if (!$a.attr('title') || $a.attr('title').trim().toLowerCase() === 'print') { - $a.attr('title', isEn ? 'Print this page' : '列印此頁'); - } - }); + // F. 最終清理:移除冗餘 label,避免螢幕閱讀器唸兩次 + $a.removeAttr('aria-label'); + }); + + // ✅ G. 補強特定連結(須在迴圈外,因為 javascript: 連結被迴圈跳過) + $('.btn-o a').each(function() { + var $a = $(this); + if (!$a.attr('target')) { + $a.attr('target', '_blank'); + $a.attr('rel', 'noopener noreferrer'); + } + $a.attr('title', isEn ? 'Share to X Open in new window' : '分享至 X 在新視窗開啟'); + }); + + $('.print-button a').each(function() { + var $a = $(this); + if (!$a.attr('title') || $a.attr('title').trim().toLowerCase() === 'print') { + $a.attr('title', isEn ? 'Print this page' : '列印此頁'); + } + }); + + $('a.i-archive__file-name').each(function () { + var $a = $(this); + var isNewWindow = ($a.attr('target') === '_blank'); + $a.attr('title', isNewWindow ? i18n.newWin : i18n.selfWin); + }); + } + + // ✅ 延遲 800ms 確保所有其他 function 執行完畢後才處理 + setTimeout(function() { + fixAccessibilityAll(); + }, 800); +}); + //圖示新增sr-only +$(function() { + function smartFixIcons() { + var isEn = $('html').attr('lang') === 'en'; + + var labels = { + calendar : isEn ? 'Date: ' : '日期:', + user : isEn ? 'Author: ' : '發布者:', + tag : isEn ? 'Tag: ' : '標籤:', + clock : isEn ? 'Time: ' : '時間:', + location : isEn ? 'Location: ' : '地點:', + phone : isEn ? 'Phone: ' : '電話:', + envelope : isEn ? 'Email: ' : '信箱:', + folder : isEn ? 'Category: ' : '分類:', + eye : isEn ? 'Views: ' : '瀏覽次數:', + download : isEn ? 'Download: ' : '下載檔案:', + search : isEn ? 'Search' : '搜尋', + bars : isEn ? 'Menu' : '選單', + print : isEn ? 'Print this page' : '列印此頁', + share : isEn ? 'Share' : '分享至社群', + close : isEn ? 'Close window' : '關閉視窗', + closePopup : isEn ? 'Close announcement' : '關閉公告視窗', + closeGallery: isEn ? 'Close gallery' : '關閉相簿', + toggleDesc : isEn ? 'Show or hide image description' : '顯示或隱藏圖片說明', + themeSwitch : isEn ? 'Switch gallery theme': '切換相簿主題', + showOriginal: isEn ? 'Show original image' : '顯示原始圖片' + }; + + $('i').each(function() { + var $icon = $(this); + var $parent = $icon.parent(); + + // 1. 所有圖示先對螢幕閱讀器隱藏 + $icon.attr('aria-hidden', 'true'); + + // 特判:.close-screen-btn 內的圖示,移除無關的 sr-only 並設正確 aria-label + if ($parent.hasClass('close-screen-btn')) { + $parent.find('.sr-only').remove(); + $parent.attr('aria-label', labels.closePopup); + if ($icon.next('.sr-only').length === 0) { + $icon.after('' + labels.closePopup + ''); } - - // ✅ 延遲 800ms 確保所有其他 function 執行完畢後才處理 - setTimeout(function() { - fixAccessibilityAll(); - }, 800); - }); - //圖示新增sr-only - $(function() { - function smartFixIcons() { - $('i').each(function() { - var $icon = $(this); - var $parent = $icon.parent(); - - // 1. 基本安全:所有圖示先對螢幕閱讀器隱藏 (避免讀出 Unicode 亂碼) - $icon.attr('aria-hidden', 'true'); - - // 2. 檢查:如果已經有 aria-label 或旁邊已有 sr-only,就不重複處理 - if ($icon.attr('aria-label') || $icon.next('.sr-only').length > 0 || $parent.find('.sr-only').length > 0) { - return; - } - - var cls = ($icon.attr('class') || '').toLowerCase(); - var labelText = ''; - - // --- 策略 A:常見資訊型圖示 (自動識別內容意義) --- - if (cls.includes('calendar')) labelText = '日期:'; - else if (cls.includes('user') || cls.includes('male')) labelText = '發布者:'; - else if (cls.includes('tag')) labelText = '標籤:'; - else if (cls.includes('clock') || cls.includes('time')) labelText = '時間:'; - else if (cls.includes('map-marker') || cls.includes('location')) labelText = '地點:'; - else if (cls.includes('phone')) labelText = '電話:'; - else if (cls.includes('envelope') || cls.includes('mail')) labelText = '信箱:'; - else if (cls.includes('folder')) labelText = '分類:'; - else if (cls.includes('eye')) labelText = '瀏覽次數:'; - else if (cls.includes('download')) labelText = '下載檔案:'; - - // --- 策略 B:功能性按鈕 (如果 i 放在 或 -
+ diff --git a/modules/personal_course/index.html.erb b/modules/personal_course/index.html.erb index 83fd717..070f753 100644 --- a/modules/personal_course/index.html.erb +++ b/modules/personal_course/index.html.erb @@ -3,19 +3,23 @@|
+
+ {{title}}
+
+
+
+
+ |
+
|---|
| {{text}} | +