diff --git a/assets/javascripts/app.js b/assets/javascripts/app.js index b16bf08..8fe60cf 100644 --- a/assets/javascripts/app.js +++ b/assets/javascripts/app.js @@ -902,7 +902,7 @@ var isLinkImage = $parentA.length > 0 && $parentA.text().replace(/\u00a0/g, '').trim() === ""; // --- 1. 處理「裝飾性圖片」標註 --- - if (currentAlt === "announcement image" || currentAlt === "裝飾圖片" || currentTitle === "裝飾圖片") { + if (currentAlt === "announcement image" || currentAlt === "裝飾圖片" || currentTitle === "裝飾圖片" || currentAlt === "這是一張圖片" || currentTitle === "這是一張圖片") { $img.attr('alt', ''); $img.removeAttr('title'); return; @@ -1188,108 +1188,132 @@ }, 800); }); - $(function() { -function smartFixIcons() { - $('i').each(function() { - var $icon = $(this); - var $parent = $icon.parent(); +$(function() { + function smartFixIcons() { + var isEn = $('html').attr('lang') === 'en'; - // 1. 所有圖示先對螢幕閱讀器隱藏 - $icon.attr('aria-hidden', 'true'); + 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' : '顯示原始圖片' + }; - // 特判:.close-screen-btn 內的圖示,移除無關的 sr-only 並設正確 aria-label - if ($parent.hasClass('close-screen-btn')) { - $parent.find('.sr-only').remove(); - $parent.attr('aria-label', '關閉公告視窗'); - if ($icon.next('.sr-only').length === 0) { - $icon.after('關閉公告視窗'); + $('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 + ''); + } + return; } - return; - } - // 特判:.gallery-close 內的圖示,移除錯誤的 sr-only「時間:」並補正確說明 - if ($parent.closest('.gallery-close').length > 0) { - $parent.closest('.gallery-close').find('.sr-only').remove(); - $parent.closest('.gallery-close').attr('aria-label', '關閉相簿'); - $parent.closest('.gallery-close').attr('role', 'button'); - $parent.closest('.gallery-close').attr('tabindex', '0'); - if ($icon.next('.sr-only').length === 0) { - $icon.after('關閉相簿'); + // 特判:.gallery-close 內的圖示,移除錯誤的 sr-only 並補正確說明 + if ($parent.closest('.gallery-close').length > 0) { + $parent.closest('.gallery-close').find('.sr-only').remove(); + $parent.closest('.gallery-close').attr('aria-label', labels.closeGallery); + $parent.closest('.gallery-close').attr('role', 'button'); + $parent.closest('.gallery-close').attr('tabindex', '0'); + if ($icon.next('.sr-only').length === 0) { + $icon.after('' + labels.closeGallery + ''); + } + return; } - return; - } - // 特判:.gallery-toggle-desc 補說明 - if ($parent.closest('.gallery-toggle-desc').length > 0) { - var $toggleBtn = $parent.closest('.gallery-toggle-desc'); - if (!$toggleBtn.attr('aria-label')) { - $toggleBtn.attr('aria-label', '顯示或隱藏圖片說明'); - $toggleBtn.attr('role', 'button'); - $toggleBtn.attr('tabindex', '0'); + // 特判:.gallery-toggle-desc 補說明 + if ($parent.closest('.gallery-toggle-desc').length > 0) { + var $toggleBtn = $parent.closest('.gallery-toggle-desc'); + if (!$toggleBtn.attr('aria-label')) { + $toggleBtn.attr('aria-label', labels.toggleDesc); + $toggleBtn.attr('role', 'button'); + $toggleBtn.attr('tabindex', '0'); + } + if ($icon.next('.sr-only').length === 0) { + $icon.after('' + labels.toggleDesc + ''); + } + return; } - if ($icon.next('.sr-only').length === 0) { - $icon.after('顯示或隱藏圖片說明'); + + // 特判:.gallery-theme-switch 補說明 + if ($parent.closest('.gallery-theme-switch').length > 0) { + var $themeBtn = $parent.closest('.gallery-theme-switch'); + if (!$themeBtn.attr('aria-label')) { + $themeBtn.attr('aria-label', labels.themeSwitch); + $themeBtn.attr('role', 'button'); + $themeBtn.attr('tabindex', '0'); + } + if ($icon.next('.sr-only').length === 0) { + $icon.after('' + labels.themeSwitch + ''); + } + return; } - return; - } - // 特判:.gallery-theme-switch 補說明 - if ($parent.closest('.gallery-theme-switch').length > 0) { - var $themeBtn = $parent.closest('.gallery-theme-switch'); - if (!$themeBtn.attr('aria-label')) { - $themeBtn.attr('aria-label', '切換相簿主題'); - $themeBtn.attr('role', 'button'); - $themeBtn.attr('tabindex', '0'); + // 2. 已有 aria-label 或 sr-only 就不重複處理 + if ($icon.attr('aria-label') || $icon.next('.sr-only').length > 0 || $parent.find('.sr-only').length > 0) { + return; } - if ($icon.next('.sr-only').length === 0) { - $icon.after('切換相簿主題'); + + var cls = ($icon.attr('class') || '').toLowerCase(); + var labelText = ''; + + // --- 策略 A:常見資訊型圖示 --- + if (cls.includes('calendar')) labelText = labels.calendar; + else if (cls.includes('user') || cls.includes('male')) labelText = labels.user; + else if (cls.includes('tag')) labelText = labels.tag; + else if (cls.includes('clock') || cls.includes('time')) labelText = labels.clock; + else if (cls.includes('map-marker') || cls.includes('location')) labelText = labels.location; + else if (cls.includes('phone')) labelText = labels.phone; + else if (cls.includes('envelope') || cls.includes('mail')) labelText = labels.envelope; + else if (cls.includes('folder')) labelText = labels.folder; + else if (cls.includes('eye')) labelText = labels.eye; + else if (cls.includes('download')) labelText = labels.download; + + // --- 策略 B:功能性按鈕 --- + var $container = $icon.closest('a, button'); + if ($container.length > 0 && $container.text().trim() === '') { + var btnTitle = $container.attr('title'); + if (btnTitle) labelText = btnTitle; + else if (cls.includes('search')) labelText = labels.search; + else if (cls.includes('bars') || cls.includes('navicon')) labelText = labels.bars; + else if (cls.includes('print')) labelText = labels.print; + else if (cls.includes('share')) labelText = labels.share; + else if (cls.includes('close') || cls.includes('times')) labelText = labels.close; + else if (cls.includes('image') || cls.includes('photo')) labelText = labels.showOriginal; } - return; - } - // 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:功能性按鈕 --- - var $container = $icon.closest('a, button'); - if ($container.length > 0 && $container.text().trim() === '') { - var btnTitle = $container.attr('title'); - if (btnTitle) labelText = btnTitle; - else if (cls.includes('search')) labelText = '搜尋'; - else if (cls.includes('bars') || cls.includes('navicon')) labelText = '選單'; - else if (cls.includes('print')) labelText = '列印此頁'; - else if (cls.includes('share')) labelText = '分享至社群'; - else if (cls.includes('close') || cls.includes('times')) labelText = '關閉視窗'; - else if (cls.includes('image') || cls.includes('photo')) labelText = '顯示原始圖片'; - } - - // --- 3. 執行補強 --- - if (labelText !== '') { - $icon.after('' + labelText + ''); - } - }); -} - - // 延遲執行確保內容渲染完成 - setTimeout(smartFixIcons, 500); + // --- 3. 執行補強 --- + if (labelText !== '') { + $icon.after('' + labelText + ''); + } }); + } + + setTimeout(smartFixIcons, 500); +}); // 刪除banner-slide的空連結和空連結目標 for(var i=0;i<$('.w-ba-banner__slide a').length;i++){ if($('.w-ba-banner__slide a').eq(i).attr('href')=="") @@ -1514,6 +1538,72 @@ function smartFixIcons() { resizeTimer = setTimeout(fixAccessKeyM, 150); }); }); + + //導覽選單子選單無障礙狀態修正 (支援多層級) + function fixNavMenuA11y() { + const $navItems = $('#main-nav li'); + + $navItems.each(function() { + const $li = $(this); + const $toggle = $li.children('a'); + const $subMenu = $li.children('ul'); + + // 判斷是否有子選單 + if ($subMenu.length > 0) { + // 1. 初始化屬性 + // aria-haspopup="true" 表示有點擊後彈出選單的功能 + // aria-expanded="false" 預設為摺疊 + $toggle.attr({ + 'aria-haspopup': 'true', + 'aria-expanded': 'false' + }); + + // 如果 href 是 javascript:void(0);,建議補上 role="button" 讓語音讀出它是按鈕 + if ($toggle.attr('href').indexOf('javascript') !== -1) { + $toggle.attr('role', 'button'); + } + + // 2. 焦點移入事件 (Focus) + $toggle.on('focus', function() { + // 這裡假設你的選單是用 CSS 或其他 JS 控制顯示, + // 當焦點進入時,將狀態改為 true + $(this).attr('aria-expanded', 'true'); + // 如果需要透過 JS 強制顯示選單可在此加上:$subMenu.addClass('show'); (視你的 CSS 而定) + }); + + // 3. 處理失焦事件 (Focusout) + // 使用 focusout 並檢查焦點是否移出了整個 li 區塊 + $li.on('focusout', function(e) { + // 使用 setTimeout 確保能抓到下一個獲焦元素 + setTimeout(function() { + if (!$li.is(':focus-within')) { + $toggle.attr('aria-expanded', 'false'); + // 如果有 class 控制顯示:$subMenu.removeClass('show'); + } + }, 50); + }); + } + }); + } + // 執行修正 + fixNavMenuA11y(); + //修正無超連結目的之連結 + $(function() { + /** + * 針對所有 href="#" 的連結改為 javascript:void(0); + * 避免使用者點擊後頁面跳回頂端,造成操作困擾 + */ + function fixEmptyHref() { + $('a[href="#"]').each(function() { + // 將 href 修改為不觸發跳轉的 void 腳本 + $(this).attr('href', 'javascript:void(0);'); + $(this).attr('role', 'button'); + }); + } + + // 延遲執行,確保動態產生的選單也能被補強 + setTimeout(fixEmptyHref, 500); + }); //公告頁籤無障礙 $(function() { function fixTabLinearOrder() { @@ -1600,71 +1690,6 @@ function smartFixIcons() { fixTabLinearOrder(); }); - //導覽選單子選單無障礙狀態修正 (支援多層級) - function fixNavMenuA11y() { - const $navItems = $('#main-nav li'); - - $navItems.each(function() { - const $li = $(this); - const $toggle = $li.children('a'); - const $subMenu = $li.children('ul'); - - // 判斷是否有子選單 - if ($subMenu.length > 0) { - // 1. 初始化屬性 - // aria-haspopup="true" 表示有點擊後彈出選單的功能 - // aria-expanded="false" 預設為摺疊 - $toggle.attr({ - 'aria-haspopup': 'true', - 'aria-expanded': 'false' - }); - - // 如果 href 是 javascript:void(0);,建議補上 role="button" 讓語音讀出它是按鈕 - if ($toggle.attr('href').indexOf('javascript') !== -1) { - $toggle.attr('role', 'button'); - } - - // 2. 焦點移入事件 (Focus) - $toggle.on('focus', function() { - // 這裡假設你的選單是用 CSS 或其他 JS 控制顯示, - // 當焦點進入時,將狀態改為 true - $(this).attr('aria-expanded', 'true'); - // 如果需要透過 JS 強制顯示選單可在此加上:$subMenu.addClass('show'); (視你的 CSS 而定) - }); - - // 3. 處理失焦事件 (Focusout) - // 使用 focusout 並檢查焦點是否移出了整個 li 區塊 - $li.on('focusout', function(e) { - // 使用 setTimeout 確保能抓到下一個獲焦元素 - setTimeout(function() { - if (!$li.is(':focus-within')) { - $toggle.attr('aria-expanded', 'false'); - // 如果有 class 控制顯示:$subMenu.removeClass('show'); - } - }, 50); - }); - } - }); - } - // 執行修正 - fixNavMenuA11y(); - //修正無超連結目的之連結 - $(function() { - /** - * 針對所有 href="#" 的連結改為 javascript:void(0); - * 避免使用者點擊後頁面跳回頂端,造成操作困擾 - */ - function fixEmptyHref() { - $('a[href="#"]').each(function() { - // 將 href 修改為不觸發跳轉的 void 腳本 - $(this).attr('href', 'javascript:void(0);'); - $(this).attr('role', 'button'); - }); - } - - // 延遲執行,確保動態產生的選單也能被補強 - setTimeout(fixEmptyHref, 500); - }); //頁籤無障礙 $(function () { function fixTabAccessibility() { @@ -3011,11 +3036,130 @@ $(function() { attributeFilter: ['style'] }); }); - // 執行 member等高計算,目前改用flexbox故mark掉 by ika 20160105 - // $(window).load(function() { - // if ($('.index-member-3').length && $(window).width() > 992) { - // ORBITFRONT.member.equalHeight('.i-member-item-inner'); - // } - // }); +//萬用表格無障礙 +function fixSearchInputA11y() { + $('input[type="search"], input[type="text"]').each(function () { + var $input = $(this); + + // 已有對應 label 就跳過 + var inputId = $input.attr('id'); + if (inputId && $('label[for="' + inputId + '"]').length) return; + + // 已有 aria-label / aria-labelledby / title 就跳過 + if ($input.attr('aria-label') || $input.attr('aria-labelledby') || $input.attr('title')) return; + + // 從 placeholder 取得說明文字 + var placeholder = $input.attr('placeholder') || ''; + + // 從最近的 th 或欄位標題取得欄位名稱 + var colName = ''; + var $th = $input.closest('th, div[class*="col-"]'); + if ($th.length) { + colName = $th.find('.universal-th-text').text().trim(); + } + + // 組合標籤文字 + var label = ''; + if (colName && placeholder) { + label = colName + '搜尋'; + } else if (colName) { + label = colName + '搜尋'; + } else if (placeholder) { + label = placeholder; + } else { + label = '搜尋關鍵字'; + } + + $input.attr('aria-label', label); + $input.attr('title', label); + }); +} + +$(document).ready(function () { + setTimeout(fixSearchInputA11y, 500); +}); +//萬用表格無障礙 +function fixSortLinkA11y() { + $('a').filter(function () { + return $(this).find('.universal-th-icon').length > 0; + }).each(function () { + var $link = $(this); + + // === Step 1: 移除 display:none 的 span === + $link.find('span').each(function () { + if ($(this).css('display') === 'none' || $(this).attr('style') && $(this).attr('style').indexOf('display: none') !== -1) { + $(this).remove(); + } + }); + + // === Step 2: 取得 aria-label === + var ariaLabel = $link.attr('aria-label') || ''; + + // 若 aria-label 空白或含未替換模板變數,從旁邊 .universal-th-text 補 + if (!ariaLabel || ariaLabel.indexOf('{{') !== -1) { + var $th = $link.closest('th, div[class*="col-"]'); + var colName = $th.find('.universal-th-text').text().trim(); + ariaLabel = colName ? '重新排序' + colName : '排序'; + $link.attr('aria-label', ariaLabel); + } + + // === Step 3: 補上 title 屬性 === + if (!$link.attr('title')) { + $link.attr('title', ariaLabel + '(在本視窗開啟)'); + } + + // === Step 4: 確保圖示有 aria-hidden === + $link.find('.universal-th-icon').attr('aria-hidden', 'true'); + + // === Step 5: 補上 visually-hidden 連結文字 === + if ($link.find('.visually-hidden').length === 0) { + $link.append('' + ariaLabel + ''); + } + }); +} +function fixSearchInputA11y() { + $('input[type="search"], input[type="text"]').each(function () { + var $input = $(this); + + // 已有對應 label 就跳過 + var inputId = $input.attr('id'); + if (inputId && $('label[for="' + inputId + '"]').length) return; + + // 已有 aria-label / aria-labelledby / title 就跳過 + if ($input.attr('aria-label') || $input.attr('aria-labelledby') || $input.attr('title')) return; + + // 從 placeholder 取得說明文字 + var placeholder = $input.attr('placeholder') || ''; + + // 從最近的 th 或欄位標題取得欄位名稱 + var colName = ''; + var $th = $input.closest('th, div[class*="col-"]'); + if ($th.length) { + colName = $th.find('.universal-th-text').text().trim(); + } + + // 組合標籤文字 + var label = ''; + if (colName && placeholder) { + label = colName + '搜尋'; + } else if (colName) { + label = colName + '搜尋'; + } else if (placeholder) { + label = placeholder; + } else { + label = '搜尋關鍵字'; + } + + $input.attr('aria-label', label); + $input.attr('title', label); + }); +} + +$(document).ready(function () { + setTimeout(function () { + fixSortLinkA11y(); + fixSearchInputA11y(); + }, 500); +}); }(jQuery, window)); diff --git a/assets/stylesheets/template/modules/announcement.scss b/assets/stylesheets/template/modules/announcement.scss index 235a9f9..037250c 100644 --- a/assets/stylesheets/template/modules/announcement.scss +++ b/assets/stylesheets/template/modules/announcement.scss @@ -2516,8 +2516,9 @@ table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before, tabl border:0; &:hover{ .i-annc__content-wrap{ - background: radial-gradient(circle at 70% 80%, #66a9e2b3, transparent 60%), radial-gradient(circle at 1% 1%, #fb8f72, transparent 30%), radial-gradient(circle at 50% 50%, #3c78ff4d, transparent 70%), linear-gradient(135deg, #2b7bbe 0%, #00437c 80%, #fb8f72 100%); + background: radial-gradient(circle at 70% 80%, #176bb2b3, transparent 60%), radial-gradient(circle at 1% 1%, #b54c30, transparent 30%), radial-gradient(circle at 50% 50%, rgba(60, 120, 255, 0.301961), transparent 70%), linear-gradient(135deg, #045ea9 0%, #00437c 80%, #cb3f19 100%); color: #fff!important; + text-shadow: 0px 0px 8px #000000; i,a,.i-annc__postdate-wrap,.i-annc__category-wrap,.i-annc__title{ color: #fff!important; }