diff --git a/assets/images/orbitek.png b/assets/images/orbitek.png new file mode 100644 index 0000000..88e3dba Binary files /dev/null and b/assets/images/orbitek.png differ diff --git a/assets/javascripts/app.js b/assets/javascripts/app.js index 0c62e34..33dd23c 100644 --- a/assets/javascripts/app.js +++ b/assets/javascripts/app.js @@ -1,35 +1,59 @@ ;(function($, win, undefined) { // ECMAScript 5 嚴格模式 'use strict'; -$(document).ready(function() { - // 確保兩個元素都存在 - if ($('.header-nav blockquote').length && $('.orbit-bar-menu').length) { - $('.header-nav blockquote').appendTo('.orbit-bar-menu'); - } -}); - //header fixed在上方 -$(function(){ - function initdata1(){ + //blockquote移動 + $(function() { + // 1. 定義變數,減少重複選取 DOM 的效能消耗 + var $signLanguageBox = $('.orbit-bar-search-sign-language'); + var $headerNav = $('.header-nav'); + var $target = $('.header-nav blockquote'); - if (!$('.homebanner').length) return; + function handleNavMove() { + var windowWidth = window.innerWidth; - // 取得 65.5em 對應的 px - var em = parseFloat($('body').css('font-size')); - var minHeight = 65.5 * em; + if (windowWidth > 768) { + if ($signLanguageBox.length && $signLanguageBox.find('blockquote').length === 0) { + $signLanguageBox.prepend($target); + } + } else { + if ($headerNav.length && $headerNav.find('blockquote').length === 0) { + $headerNav.append($target); // 或者用 .prepend() 取決於你要放的位置 + } + } + } - // 判斷 .layout-content 高度 - if ($('.layout-content').height() < minHeight) return; + // 初始執行一次 + handleNavMove(); - var scrollTop = $(window).scrollTop(); - $('.outdropdowns').toggleClass('navFixed', scrollTop > 5); - } - - initdata1(); - - $(window).on('scroll', function(){ - requestAnimationFrame(initdata1); + // 監聽視窗縮放 + $(window).on('resize', function() { + // 加上防抖 (Debounce) 可以讓效能更好,但若要即時反應可直接執行 + handleNavMove(); + }); + }); + //header fixed在上方 + $(function(){ + function initdata1(){ + + if (!$('.homebanner').length) return; + + // 取得 65.5em 對應的 px + var em = parseFloat($('body').css('font-size')); + var minHeight = 65.5 * em; + + // 判斷 .layout-content 高度 + if ($('.layout-content').height() < minHeight) return; + + var scrollTop = $(window).scrollTop(); + $('.outdropdowns').toggleClass('navFixed', scrollTop > 5); + } + + initdata1(); + + $(window).on('scroll', function(){ + requestAnimationFrame(initdata1); + }); }); -}); // 初始函數: 把所有的程式碼都包在init裡面,方便在之後的jQuery ready 函數裡一次呼叫 function init() { // 快取一些常用的變數 @@ -333,7 +357,68 @@ $(function(){ $that.removeClass('opened'); } }) + }, + SiteMenuHandler: function() { + /** + * 處理 .sitemenu-dropdown-toggle 點擊滑動效果 + */ + $(document).on('click', '.sitemenu-dropdown-toggle', function(e) { + e.preventDefault(); + e.stopPropagation(); + + var $toggle = $(this); + var $subMenu = $toggle.next('.sitemenu-list'); + var isOpened = $toggle.hasClass('opened'); + var isLevel1 = $toggle.parent().hasClass('level-1'); + + // 設定滑動速度 (毫秒),400-500ms 是最舒適的滑動感 + var duration = 400; + + if (!isOpened) { + /** --- 展開選單 --- **/ + + if (isLevel1) { + // 手風琴:關閉其他第一層,並將其箭頭轉回 down + var $otherL1 = $('.sitemenu-item.level-1 > .sitemenu-dropdown-toggle').not($toggle); + $otherL1.removeClass('opened fa-caret-up').addClass('fa-caret-down'); + $otherL1.siblings('.sitemenu-list').stop(true, true).slideUp(duration); + } else { + // 深層手風琴:僅關閉同父層下的其他子選單 + var $otherDeep = $toggle.parent().siblings().find('.sitemenu-dropdown-toggle'); + $otherDeep.removeClass('opened fa-caret-up').addClass('fa-caret-down'); + $toggle.parent().siblings().find('.sitemenu-list').stop(true, true).slideUp(duration); + } + + // 執行滑動展開 + $subMenu.stop(true, true).slideDown(duration, function() { + // 核心修正:展開後強制抹除 Bootstrap Popper.js 的位移,防止選單歪掉 + $(this).css({ + 'transform': 'none', + 'top': 'auto', + 'left': 'auto', + 'position': 'static', + 'display': 'block' // 確保狀態正確 + }); + }); + + // 切換圖示:down -> up + $toggle.addClass('opened').removeClass('fa-caret-down').addClass('fa-caret-up'); + + } else { + /** --- 收合選單 --- **/ + + $subMenu.stop(true, true).slideUp(duration); + + // 切換圖示:up -> down + $toggle.removeClass('opened fa-caret-up').addClass('fa-caret-down'); + + // 遞迴重設:關閉父層時,確保內部子層也收合且箭頭向下 + $subMenu.find('.sitemenu-list').stop(true, true).slideUp(duration); + $subMenu.find('.sitemenu-dropdown-toggle').removeClass('opened fa-caret-up').addClass('fa-caret-down'); + } + }); } + }; // 把orbit物件加到window物件裡面並改名為ORBITFRONT來減少名稱衝突的機會 @@ -371,6 +456,7 @@ $(function(){ $('.mobile-menu').append($('.modules-menu')); $('.header-buttom').appendTo($('.modules-menu')); orbit.ClickMenuHandler(); + orbit.SiteMenuHandler(); } // 自適應網頁使用,當使用者改變瀏覽器寬度時呼叫orbit.nav.setDropdown函數 @@ -387,8 +473,7 @@ $(function(){ $('.header-buttom').appendTo($('.modules-menu')); orbit.MobileMenu(); orbit.ClickMenuHandler(); - // $('.navbar-toggle').bind('click', orbit.ClickMenuHandler); - // $('.navbar-toggle').bind(orbit.ClickMenuHandler()); + orbit.SiteMenuHandler(); } },500 )} else { resizeTimer = setTimeout(function(){ @@ -736,55 +821,36 @@ $(function(){ $this.attr('title', '按鈕'); } }); - //img無障礙 - $('img').each(function() { - var $img = $(this); - var currentAlt = $img.attr('alt'); - var currentTitle = $img.attr('title'); - // --- 1. 處理裝飾性圖片 (最高優先權) --- - // 若開發者標註為 "裝飾圖片",則清空 alt 並移除 title,這是 AA 級的最優解 - if (currentAlt === "裝飾圖片" || currentTitle === "裝飾圖片") { - $img.attr('alt', ''); - $img.removeAttr('title'); - return; // 處理完畢,跳過這張圖 - } + function removeEmptyTitles() { + // 定義所有需要檢查的 class + const targetClasses = [ + '.w-annc__widget-title', + '.widget-title', + '.show-title', + '.event-annc-title', + '.annc-title', + '.sitemenu-title', + '.widget-link__widget-title' + ]; - // --- 2. 處理缺失 alt 或空的 alt (需要補強的圖片) --- - if (typeof currentAlt === 'undefined' || currentAlt.trim() === '') { - - let finalAlt = ""; // 預設值 + // 將陣列轉換為 jQuery 選擇器字串 + const selector = targetClasses.join(', '); - // A. 嘗試從父層 (如 ) 的 title 抓取描述 - var parentTitle = $img.parent().attr('title'); - - // B. 嘗試抓取同區域內的標題文字 (常見於新聞/公告列表) - // 會往上找最近的連結,再往下找標題標籤 - var nearbyTitle = $img.closest('a').find('h1, h2, h3, h4, h5, h6').first().text().trim(); + $(selector).each(function() { + // 使用 $.trim 移除空格、換行符號 + // 然後檢查裡面是否完全沒有文字 + const textContent = $.trim($(this).text()); - if (parentTitle && parentTitle.trim() !== '') { - finalAlt = parentTitle.trim(); - } else if (nearbyTitle !== '') { - finalAlt = nearbyTitle; - } - - // 3. 寫入結果 - // 如果 finalAlt 仍為空,則設為 "" (這符合 AA 規範,將無來源的圖視為裝飾) - $img.attr('alt', finalAlt); - - // 4. 同步處理 title 屬性 - // 在 AA 級中,如果 img 已經有 alt,通常不需要重複的 title - if (currentTitle && currentTitle.trim() === finalAlt) { - $img.removeAttr('title'); // 移除重複資訊,避免部分閱讀器讀兩次 - } - } - }); - $(".w-annc__img-wrap a").each(function () { - var $this = $(this); - // 確保 內沒有文字節點 (避免重複添加) - if ($this.text().trim() === "") { - $this.append('公告圖片'); - } + if (textContent === "") { + // 如果沒有文字,則將整個元素刪除 + $(this).remove(); + } + }); + } + // 執行 function + $(document).ready(function() { + removeEmptyTitles(); }); $(".widget-link__widget-title").each(function () { if ($(this).text().trim() === "") { @@ -838,105 +904,346 @@ $(function(){ $button.attr('aria-label', '播放驗證碼語音'); } }); - //有連結目的之所有a標籤加上aria-label和title - $('a').each(function() { - var $a = $(this); - var href = $a.attr('href'); - if (!href) return; + // img無障礙 + $(function() { + /** + * 圖片無障礙 AA 級補強 (人工檢測優化版) + */ + function fixImageAccessibility() { + $('img').each(function() { + var $img = $(this); + var $parentA = $img.closest('a'); // 取得父層連結 + var currentAlt = ($img.attr('alt') || '').trim(); + var currentTitle = ($img.attr('title') || '').trim(); + + // 判定該連結是否「僅靠圖片傳達目的」(連結內無文字) + var isLinkImage = $parentA.length > 0 && $parentA.text().replace(/\u00a0/g, '').trim() === ""; - // 1. 檢查是否有 .sr-only (給螢幕閱讀器專用的隱藏文字) - var hasSrOnly = $a.find('.sr-only').length > 0; + // --- 1. 處理「裝飾性圖片」標註 --- + if (currentAlt === "announcement image" || currentAlt === "裝飾圖片" || currentTitle === "裝飾圖片") { + $img.attr('alt', ''); + $img.removeAttr('title'); + return; + } - // 若有 .sr-only,移除屬性避免重複朗讀,這是最符合 AA 的作法 - if (hasSrOnly) { - $a.removeAttr('aria-label'); - $a.removeAttr('title'); - $a.removeAttr('alt'); // a 標籤不應有 alt - return; - } + // --- 2. 解決【alt 與 title 相同】的問題 --- + if (currentAlt !== '' && currentAlt === currentTitle) { + $img.removeAttr('title'); + currentTitle = ''; + } - // 2. 移除 a 標籤上錯誤的 alt 屬性 (AA級規範 a 不可有 alt,alt 應在 img 上) - if ($a.attr('alt')) { - $a.removeAttr('alt'); - } + // --- 3. 處理「缺失 alt」或「空的 alt」補強邏輯 --- + // 修改點:若是具有連結用途的圖片,alt 不可為空 + if (currentAlt === '') { + let finalAlt = ""; - // 3. 開始組合描述字串 - let contentDesc = ''; // 用來存放連結本身的內容描述 - let windowTask = ''; // 用來存放開啟視窗的提示 + // A. 優先嘗試從父層 () 的 title 抓取 + var parentATitle = $parentA.attr('title'); + + // B. 嘗試抓取同區域內的標題文字 + var nearbyTitle = $parentA.find('h1, h2, h3, h4, h5, h6').first().text().trim(); - // --- 視窗開啟方式判定 --- - if ($a.attr('target') === '_blank') { - windowTask = '(在新視窗開啟)'; - } else if ($a.attr('target') === '_self') { - windowTask = '(在本視窗開啟)'; - } + if (parentATitle && parentATitle.trim() !== '') { + // 移除 title 內的視窗提示字眼,只取純文字作為 alt + finalAlt = parentATitle.replace(/\(在本視窗開啟\)|\(在新視窗開啟\)/g, '').trim(); + } else if (nearbyTitle !== '') { + finalAlt = nearbyTitle; + } else if (isLinkImage) { + // 如果是連結圖片且都抓不到文字,給予預設值「連結」避免檢測失敗 + finalAlt = "連結"; + } - // --- 連結內容判定 --- - var $img = $a.find('img'); - if ($img.length > 0) { - // 優先抓取圖片的 alt,如果沒有 alt 則嘗試抓取圖片的 title - contentDesc = $img.attr('alt') || $img.attr('title') || ''; - } + $img.attr('alt', finalAlt); - // 如果圖片沒描述,或者根本沒圖片,則抓取連結內的文字 - if (!contentDesc.trim()) { - contentDesc = $a.text().trim(); - } - - // 如果還是沒文字,檢查是否只有單個 span - if (!contentDesc.trim() && $a.children('span').length === 1) { - contentDesc = $a.children('span').text().trim(); - } - - // --- 最終組合 --- - // 確保內容描述在前,視窗提示在後,這符合語音操作邏輯 - let finalLabel = (contentDesc + ' ' + windowTask).trim(); - - if (finalLabel) { - // 4. 同步設定 aria-label (輔具優先) 與 title (滑鼠提示) - // 如果原本沒有設定,才進行補強 - if (!$a.attr('aria-label')) { - $a.attr('aria-label', finalLabel); + // --- 4. 二次清理 --- + if (currentTitle !== '' && currentTitle === finalAlt) { + $img.removeAttr('title'); + } + } + }); } - if (!$a.attr('title')) { - $a.attr('title', finalLabel); + + 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'; + + var pageLang = $('html').attr('lang') || 'zh-Hant'; + var isEn = pageLang.toLowerCase().startsWith('en'); + + 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'); } - } - }); - if (location.href.search('editmode=on') === -1){ - $('a').each(function () { - const $link = $(this); - const linkText = $link.text().trim(); - $link.find('img').each(function () { - const $img = $(this); - const altText = $img.attr('alt')?.trim(); + // --- Part 2: 全站連結與圖片邏輯處理 --- + $('a').each(function() { + var $a = $(this); - // 如果圖片 alt 是空的,就略過(已符合) - if (altText === '') return; - - // 如果圖片 alt 等於文字內容,就將 alt 清空,避免重複 - if (altText === linkText) { - $img.attr('alt', ''); + // 【條件 1:排除特定 class】 + if ($a.hasClass('navbar-brand')) { + return; } - }); + + 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; + } + + // --- 【修正 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; + } + + // 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', ''); + } + + 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; + + 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(); + + 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); + } + } + + // F. 最終清理:移除冗餘 label,避免螢幕閱讀器唸兩次 + $a.removeAttr('aria-label'); }); - $('a').each(function () { - const $a = $(this); - // 取得可見文字(去掉空白) - const text = $a.text().replace(/\u00a0/g, '').trim(); + // ✅ 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 在新視窗開啟'); + }); - // 是否有圖片 - const hasImg = $a.find('img').length > 0; + $('.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' : '列印此頁'); + } + }); + } - // 若沒有文字、也沒有圖片 → 移除 - if (text === '' && !hasImg) { - $a.remove(); + // ✅ 延遲 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 放在