Enhanced RSROC Events Calendar

: Extract event details and display them on the calendar page

// ==UserScript==
// @name Enhanced RSROC Events Calendar
// @namespace http://tampermonkey.net/
// @version 0.7
// @description : Extract event details and display them on the calendar page
// @author Cheng Hsien Tsou
// @match https://www.rsroc.org.tw/action/*
// @match https://www.rsroc.org.tw/action/actions_onlinedetail.asp*
// @grant none
// @license MIT
// ==/UserScript==

/*
這個script使用來擷取RSROC網站上的活動資訊,
並將教育積分時數顯示在活動頁面上。
產生google calendar的連結,
並在滑鼠懸停時顯示活動內容和聯絡資訊的tooltip。
*/


// Immediately-invoked function expression (IIFE) to encapsulate the script
(function() {
    'use strict';

    /**
     * Helper function to extract text content from a table cell based on its header.
     * @param {Document} doc - The DOM document to query (can be `document` for current page or a parsed HTML document).
     * @param {string} headerText - The text content of the `<th>` element to match.
     * @param {string} selector - CSS selector for the table rows to search within.
     * @param {boolean} isHtml - If true, returns innerHTML; otherwise, returns innerText.
     * @returns {string} The trimmed text content of the corresponding `<td>` or an empty string if not found.
     */
    function getTableCellText(doc, headerText, selector = '.articleContent table tr', isHtml = false) {
        const rows = doc.querySelectorAll(selector);
        for (const row of rows) {
            const th = row.querySelector('th');
            // Check if the header text includes the target headerText
            if (th && th.innerText.includes(headerText)) {
                const td = row.querySelector('td');
                if (td) {
                    return isHtml ? td.innerHTML.trim() : td.innerText.trim();
                }
            }
        }
        return '';
    }

    /**
     * Extracts event details from a given Document object.
     * This function consolidates the logic for fetching details from both the current page
     * and asynchronously fetched event pages.
     * @param {Document} doc - The DOM document to extract details from.
     * @returns {object} An object containing extracted event details.
     */
    function extractEventDetailsFromDocument(doc) {
        let educationPoints = getTableCellText(doc, '教育積點');
        // Remove specific redundant text from education points
        if (educationPoints) {
            educationPoints = educationPoints.replace('放射診斷科專科醫師', '').trim();
        }

        const recognizedHours = getTableCellText(doc, '認定時數');
        const eventDateTime = getTableCellText(doc, '活動日期');
        const eventLocation = getTableCellText(doc, '活動地點');
        // Get event content (can be HTML)
        let eventContent = getTableCellText(doc, '活動內容', '.articleContent table tr', true);
        // Get event description (can be HTML)
        const eventDescription = getTableCellText(doc, '活動說明', '.articleContent table tr', true);
        const contactInfo = getTableCellText(doc, '聯絡資訊');
        const eventOrganizer = getTableCellText(doc, '主辦單位'); // Extract organizer explicitly

        // Combine event content and description if both exist and are different
        // This addresses the user's request to merge "活動說明" with "活動內容".
        if (eventContent && eventDescription && eventContent !== eventDescription) {
            eventContent = `${eventContent}<br><br>${eventDescription}`;
        } else if (!eventContent && eventDescription) {
            // If only description exists, use it as content
            eventContent = eventDescription;
        }

        let eventTitle = '';
        const caption = doc.querySelector('.tableContent caption');
        if (caption) {
            eventTitle = caption.innerText.trim();
        } else {
            // Fallback for event title if caption is not found, use the extracted organizer
            eventTitle = eventOrganizer;
        }
        // Default title if no title is found
        if (!eventTitle) {
            eventTitle = 'Event';
        }

        // Remove patterns like (digits) from the event title
        eventTitle = eventTitle.replace(/\(\d+\)/g, '').trim();

        return { educationPoints, recognizedHours, eventDateTime, eventLocation, eventTitle, eventContent, contactInfo, eventOrganizer };
    }

    /**
     * Fetches event details from a given URL by making an asynchronous request.
     * @param {string} url - The URL of the event page.
     * @returns {Promise<object>} A promise that resolves to an object containing event details.
     */
    async function fetchEventDetails(url) {
        try {
            const response = await fetch(url);
            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            return extractEventDetailsFromDocument(doc);
        } catch (error) {
            console.error('Error fetching event details:', error);
            // Return default values in case of an error
            return {
                educationPoints: 'N/A',
                recognizedHours: 'N/A',
                eventDateTime: '',
                eventLocation: '',
                eventTitle: 'Event',
                eventContent: '',
                contactInfo: ''
            };
        }
    }

    /**
     * Formats a date and time string into the Google Calendar URL format.
     * Expected format: YYYY/MM/DD 星期X HH:MM ~ HH:MM
     * Google Calendar format: YYYYMMDDTHHMMSS/YYYYMMDDTHHMMSS
     * @param {string} dateTimeString - The date and time string to format.
     * @returns {string|null} The formatted date string or null if the format doesn't match.
     */
    function formatGoogleCalendarDate(dateTimeString) {
        const parts = dateTimeString.match(/(\d{4})\/(\d{2})\/(\d{2}).*?(\d{2}):(\d{2})\s*~*\s*(\d{0,2})*:*(\d{0,2})/);
        if (!parts) return null;

        const year = parts[1];
        const month = parts[2];
        const day = parts[3];
        const startHour = parts[4];
        const startMinute = parts[5];
        const endHour = parts[6] || startHour; // Assume same hour if end hour is missing
        const endMinute = parts[7] || startMinute; // Assume same minute if end minute is missing

        const start = `${year}${month}${day}T${startHour}${startMinute}00`;
        const end = `${year}${month}${day}T${endHour}${endMinute}00`;

        return `${start}/${end}`;
    }

    /**
     * Generates a Google Calendar URL based on event details.
     * @param {object} details - An object containing event details.
     * @param {string} originalUrl - The original URL of the event page.
     * @returns {string|null} The Google Calendar URL or null if date formatting fails.
     */
    function generateGoogleCalendarUrl(details, originalUrl) {
        const googleCalendarDate = formatGoogleCalendarDate(details.eventDateTime);
        if (!googleCalendarDate) return null;

        // Construct the details string for the Google Calendar event
        const calendarDetails =
            `時間: ${details.eventDateTime}\n` +
            `地點: ${details.eventLocation}\n` +
            `主辦單位: ${details.eventOrganizer || 'N/A'}\n` + // Added organizer field
            `教育積點: ${details.educationPoints}\n` +
            `認定時數: ${details.recognizedHours}\n` +
            `活動內容: ${details.eventContent}\n\n` + // This now includes merged content/description
            `聯絡資訊: ${details.contactInfo}\n\n` +
            `原始連結: ${originalUrl}`;

        return `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(details.eventTitle)}&dates=${googleCalendarDate}&details=${encodeURIComponent(calendarDetails)}&location=${encodeURIComponent(details.eventLocation)}`;
    }

    /**
     * Adds a Google Calendar link to the event detail page.
     * This function runs when the user is on a specific event detail page.
     */
    function addGoogleCalendarLinkToDetailPage() {
        // Extract details from the current document
        const details = extractEventDetailsFromDocument(document);
        const tableContent = document.querySelector('.tableContent'); // Get the table containing the caption

        if (tableContent && details.eventDateTime) {
            const googleCalendarLinkHref = generateGoogleCalendarUrl(details, window.location.href);
            if (googleCalendarLinkHref) {
                const googleCalendarLink = document.createElement('a');
                googleCalendarLink.href = googleCalendarLinkHref;
                googleCalendarLink.target = '_blank';
                googleCalendarLink.innerText = '📅 加入 Google 日曆'; // Button text
                // Apply styling for the link
                googleCalendarLink.style.display = 'block';
                googleCalendarLink.style.marginTop = '10px';
                googleCalendarLink.style.padding = '8px 12px';
                googleCalendarLink.style.backgroundColor = '#4285F4';
                googleCalendarLink.style.color = 'white';
                googleCalendarLink.style.textDecoration = 'none';
                googleCalendarLink.style.borderRadius = '4px';
                googleCalendarLink.style.width = 'fit-content';
                googleCalendarLink.style.fontWeight = 'bold';

                // Insert the link after the table containing the caption
                tableContent.parentNode.insertBefore(googleCalendarLink, tableContent.nextSibling);
            }
        }
    }

    /**
     * Main function to add event details and Google Calendar links to the calendar listing page.
     * This function runs when the user is on the main calendar listing page.
     */
    async function addEventDetailsToCalendarPage() {
        const eventLinks = document.querySelectorAll('.eventLink');

        // Create a single tooltip element to be reused
        const tooltip = document.createElement('div');
        tooltip.style.cssText = `
            position: absolute;
            background-color: #fff;
            border: 1px solid #ccc;
            padding: 10px;
            z-index: 1000;
            display: none;
            font-size: 0.9em;
            color: #333;
            max-width: 300px;
            word-wrap: break-word;
            pointer-events: none; /* Allows clicks to pass through to elements behind the tooltip */
        `;
        document.body.appendChild(tooltip);

        // Iterate over each event link on the page
        for (const link of eventLinks) {
            const url = link.href;
            const eventDiv = link.querySelector('div.event');

            // Clean the visible text of the eventDiv by removing patterns like (digits)
            if (eventDiv) {
                eventDiv.innerText = eventDiv.innerText.replace(/\(\d+\)/g, '').trim();
            }

            // Fetch detailed information for each event
            const details = await fetchEventDetails(url);

            // Store fetched details as dataset attributes on the eventDiv for easy access during hover
            eventDiv.dataset.eventContent = details.eventContent;
            eventDiv.dataset.contactInfo = details.contactInfo;
            eventDiv.dataset.eventTitle = details.eventTitle;
            eventDiv.dataset.eventDateTime = details.eventDateTime;
            eventDiv.dataset.eventLocation = details.eventLocation;
            eventDiv.dataset.educationPoints = details.educationPoints;
            eventDiv.dataset.recognizedHours = details.recognizedHours;
            eventDiv.dataset.eventOrganizer = details.eventOrganizer; // Store organizer

            // Display education points and recognized hours if available
            if (details.educationPoints !== 'N/A' || details.recognizedHours !== 'N/A' || details.eventDateTime) {
                const moreInfoDiv = document.createElement('div');
                moreInfoDiv.classList.add('moreinfo');
                moreInfoDiv.style.fontSize = '0.8em';
                moreInfoDiv.style.color = 'gray';
                moreInfoDiv.innerHTML = `${details.educationPoints}<br/>時數: ${details.recognizedHours}`;

                // Add Google Calendar icon link if event date/time is available
                if (details.eventDateTime) {
                    const googleCalendarLinkHref = generateGoogleCalendarUrl(details, url);
                    if (googleCalendarLinkHref) {
                        const googleCalendarLink = document.createElement('a');
                        googleCalendarLink.href = googleCalendarLinkHref;
                        googleCalendarLink.target = '_blank';
                        googleCalendarLink.innerText = '📅'; // Calendar icon
                        googleCalendarLink.style.marginLeft = '5px';
                        moreInfoDiv.appendChild(googleCalendarLink);
                    }
                }

                eventDiv.appendChild(moreInfoDiv);
            }

            // Add hover event listeners to show/hide the tooltip
            eventDiv.addEventListener('mouseover', (event) => {
                // Retrieve details from dataset attributes
                let content = eventDiv.dataset.eventContent || '無活動內容';
                // Replace HTML break tags and non-breaking spaces with newlines for tooltip readability
                content = content.replace(/<br\s*\/?>/g, '\n').replace(/&nbsp;/g, ' ');
                const contact = eventDiv.dataset.contactInfo || '無聯絡資訊';
                const dateTime = eventDiv.dataset.eventDateTime || '無活動時間';
                const location = eventDiv.dataset.eventLocation || '無活動地點';

                // Populate tooltip with event details
                tooltip.innerHTML =
                    `<strong>時間:</strong><br>${dateTime}<br><br>` +
                    `<strong>地點:</strong><br>${location}<br><br>` +
                    `<strong>主辦單位:</strong><br>${eventDiv.dataset.eventOrganizer || 'N/A'}<br><br>` + // Added organizer to tooltip
                    `<strong>教育積點:</strong><br>${eventDiv.dataset.educationPoints || '無教育積點'}<br><br>` +
                    `<strong>認定時數:</strong><br>${eventDiv.dataset.recognizedHours || '無認定時數'}<br><br>` +
                    `<strong>活動內容:</strong><br>${content}<br><br>` + // This now includes merged content/description
                    `<strong>聯絡資訊:</strong><br>${contact}`;

                // Position the tooltip relative to the hovered element
                const rect = event.target.getBoundingClientRect();
                tooltip.style.left = `${rect.left + window.scrollX}px`;
                tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`;
                tooltip.style.display = 'block'; // Show the tooltip
            });

            eventDiv.addEventListener('mouseout', () => {
                tooltip.style.display = 'none'; // Hide the tooltip
            });
        }
    }

    // Run the appropriate function based on the current page URL
    window.addEventListener('load', () => {
        if (window.location.href.startsWith('https://www.rsroc.org.tw/action/actions_onlinedetail.asp')) {
            addGoogleCalendarLinkToDetailPage();
        } else if (window.location.href.startsWith('https://www.rsroc.org.tw/action/')) {
            addEventDetailsToCalendarPage();
        }
    });

})();
长期地址
遇到问题?请前往 GitHub 提 Issues,或加Q群1031348184

赞助商

Fishcpy

广告

Rainyun

注册一下就行

Rainyun

一年攒够 12 元