NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Advanced Twins for University of Tsukuba // @namespace https://github.com/refiaa // @version 240429.1605 // @description Provide Advanced function for Twins (University of Tsukuba) // @author refiaa // @match https://twins.tsukuba.ac.jp/campusweb/* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; class KeyObserver { constructor(targetSelector) { this.targetSelector = targetSelector; this.currentKey = ''; this.latestKey = ''; this.displayElement = null; this.logElement = null; this.inputField = null; this.addButton = null; this.targetIframe = null; this.init(); this.initSessionStorage(); this.logAnalyzer = new LogAnalyzer(); } init() { this.findTargetIframe(); if (this.targetIframe) { this.initObserver(); this.initUIComponents(); } } // SESSION STORAGE LOGIC initSessionStorage() { const logKey = 'flowExecutionLogs'; if (!sessionStorage.getItem(logKey)) { sessionStorage.setItem(logKey, JSON.stringify([])); } } addLog(type, key, error = null) { const logKey = 'flowExecutionLogs'; const currentLogs = JSON.parse(sessionStorage.getItem(logKey)); const log = { type: type, key: key, timestamp: new Date().toISOString(), error: error }; currentLogs.push(log); sessionStorage.setItem(logKey, JSON.stringify(currentLogs)); } // END OF SESSION STORAGE LOGIC findTargetIframe() { const iframes = document.getElementsByTagName('iframe'); for (const iframe of iframes) { if (iframe.src.includes('campussquare.do?_flowId=RSW0001000-flow')) { this.targetIframe = iframe; console.log('Target iframe found:', this.targetIframe); break; } } if (!this.targetIframe) { console.warn('Target iframe not found'); } } initObserver() { if (!this.targetIframe) { console.error('Target iframe not found'); return; } try { const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document; const targetNode = targetDocument.querySelector(this.targetSelector); if (!targetNode) { console.error('Target element not found in the iframe'); return; } const config = { attributes: true, childList: true, subtree: true }; const observer = new MutationObserver(mutations => this.handleMutations(mutations)); observer.observe(targetNode, config); } catch (error) { console.error('Error accessing iframe:', error); } } handleMutations(mutations) { mutations.forEach(mutation => { if (mutation.type === 'childList' || mutation.type === 'attributes') { this.handleMutation(mutation); } }); } handleMutation(mutation) { try { const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document; const target = targetDocument.querySelector('input[name="_flowExecutionKey"]'); if (target && target.value !== this.currentKey) { this.currentKey = target.value; this.latestKey = this.currentKey; this.addLog('childList', this.currentKey); } } catch (error) { this.addLog('error', 'handleMutation', error.message); console.error('Error handling mutation:', error); } } initUIComponents() { this.createDisplay(); this.setupInputField(); this.setupButtons(); } createDisplay() { try { const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document; const uiPanel = targetDocument.createElement('div'); uiPanel.id = 'keyObserverUI'; uiPanel.classList.add('key-observer-panel'); const targetParagraph = targetDocument.querySelector('table[border="0"][cellspacing="1"][cellpadding="1"] + p'); if (targetParagraph) { targetParagraph.parentNode.insertBefore(uiPanel, targetParagraph); console.log('UI panel created:', uiPanel); } else { console.warn('Target paragraph not found, appending UI panel to body'); targetDocument.body.appendChild(uiPanel); } this.displayElement = uiPanel; this.logElement = targetDocument.createElement('div'); this.logElement.classList.add('key-observer-log'); this.displayElement.appendChild(this.logElement); } catch (error) { console.error('Error creating display:', error); } } setupInputField() { try { const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document; this.inputField = targetDocument.createElement('input'); this.inputField.id = 'jikanwariCodeInput'; this.inputField.type = 'text'; this.inputField.placeholder = '時間割コード'; this.inputField.classList.add('jikanwari-code-input'); this.displayElement.appendChild(this.inputField); } catch (error) { console.error('Error setting up input field:', error); } } setupButtons() { const actions = ['Add']; actions.forEach(action => this.createButton(action)); } createButton(action) { try { const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document; const button = targetDocument.createElement('button'); button.textContent = action; button.classList.add('key-observer-button'); button.addEventListener('click', () => this.executeCommand(action)); this.displayElement.appendChild(button); if (action === 'Add') { this.addButton = button; } } catch (error) { console.error('Error creating button:', error); } } executeCommand(action, jikanwariShozokuCode, yobi, jigen, jikanwariCode) { if (action === 'Add') { jikanwariCode = this.inputField.value.trim(); if (!jikanwariCode) { alert('時間割コードを入力してください。'); return; } } const config = this.getButtonConfig(action, jikanwariShozokuCode, yobi, jigen); const finalizeAction = () => { this.sendRequest('back', this.getButtonConfig('back')) .then(() => { this.refreshTimetable(); if (action === 'Add') { this.clearInputField(); } }) .catch(error => { console.error('Error during finalization:', error); }); }; if (action === 'Add') { this.sendRequest('input', { ...config.inputParams, jikanwariCode }) .then(() => { this.sendRequest('insert', { nendo: new Date().getFullYear().toString(), jikanwariCode }) .then(finalizeAction) .catch(error => { console.error('Error during the insert process:', error); finalizeAction(); }); }) .catch(error => { console.error('Error during the first request in insert with input:', error); finalizeAction(); }); } else if (action === 'delete') { this.sendRequest('delete', { ...config, jikanwariCode }) .then(() => { this.sendRequest('delete', { _flowExecutionKey: this.latestKey || this.currentKey }) .then(finalizeAction) .catch(error => { console.error('Error during the delete process:', error); finalizeAction(); }); }) .catch(error => { console.error('Error during the delete process:', error); finalizeAction(); }); } } clearInputField() { this.inputField.value = ''; } getButtonConfig(action, jikanwariShozokuCode, yobi, jigen) { const configs = { add: { inputParams: { yobi: 9, jigen: 0 } }, delete: { nendo: new Date().getFullYear().toString(), jikanwariShozokuCode, yobi, jigen }, back: {} }; return configs[action] || {}; } sendRequest(eventId, params) { const keyToUse = this.latestKey || this.currentKey; const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: this.encodeParams({ ...params, _flowExecutionKey: keyToUse, _eventId: eventId }) }; return fetch(`/campusweb/campussquare.do`, requestOptions) .then((response) => response.text()) .then((html) => { this.handleResponse(html, eventId); this.logAnalyzer.analyzeLogs(); }) .catch((error) => { console.error('Error with AJAX request:', error); }); } handleResponse(html, eventId) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const newKey = doc.querySelector('input[name="_flowExecutionKey"]')?.value; if (newKey) { this.latestKey = newKey; this.addLog(eventId, newKey); } else { throw new Error('Failed to fetch new key'); } } encodeParams(params) { return Object.keys(params) .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key])) .join('&'); } refreshTimetable() { return new Promise((resolve, reject) => { const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: this.encodeParams({ _flowExecutionKey: this.latestKey || this.currentKey }) }; fetch(`/campusweb/campussquare.do?_flowId=RSW0001000-flow`, requestOptions) .then(response => response.text()) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const newTimetable = doc.querySelector('table.rishu-koma'); if (newTimetable) { const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document; const currentTimetable = targetDocument.querySelector('table.rishu-koma'); if (currentTimetable) { currentTimetable.parentNode.replaceChild(newTimetable, currentTimetable); resolve(); } else { reject('Current timetable not found'); } } else { reject('New timetable not found'); } }) .catch(error => { console.error('Error refreshing timetable:', error); reject(error); }); }); } } class AdvancedSyllabus { constructor(keyObserver) { this.keyObserver = keyObserver; this.targetIframe = keyObserver.targetIframe; this.init(); } init() { this.injectStyles(); this.observePageChanges(); } injectStyles() { const styleElement = document.createElement('style'); styleElement.textContent = ` .syllabus-link { color: blue !important; cursor: pointer !important; text-decoration: underline !important; margin-left: 5px; } .delete-button { background-color: transparent; font-weight: bold; color: #333; border: none; border-radius: 0; width: 24px; height: 24px; position: absolute; top: 10px; right: 10px; transform: translate(50%, -50%); cursor: pointer; z-index: 1; } .delete-button:hover { color: #ff0000; text-decoration: none; } .delete-button::before { content: "科目を削除"; visibility: hidden; color: #fff; background-color: #555; padding: 5px 10px; border-radius: 6px; position: absolute; z-index: 1000; left: 120%; top: -75%; transform: translate(-50%, -50%); white-space: nowrap; font-size: 12px; box-shadow: 0px 2px 5px rgba(0,0,0,0.2); transition: visibility 0.2s, opacity 0.2s ease; opacity: 0; } .delete-button:hover::before { visibility: visible; opacity: 1; } `; try { const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document; targetDocument.head.appendChild(styleElement); } catch (error) { console.error('Error injecting styles:', error); } } observePageChanges() { const targetNode = document.body; const observerOptions = { childList: true, subtree: true }; const observer = new MutationObserver(mutationsList => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { this.addLinksToTimetable(); } } }); observer.observe(targetNode, observerOptions); } addLinksToTimetable() { if (!this.targetIframe) { console.warn('Target iframe not found, skipping adding links to timetable'); return; } try { const targetDocument = this.targetIframe.contentDocument || this.targetIframe.contentWindow.document; const timetableCells = targetDocument.querySelectorAll('td[bgcolor="#ffcc99"], td[bgcolor="#ffffcc"]'); timetableCells.forEach(cell => { this.addLinkToCell(cell, false); this.addDeleteButton(cell); }); const concentratedRows = targetDocument.querySelectorAll('table.rishu-etc tr[bgcolor="#ffcc99"]'); concentratedRows.forEach(row => { const syllabusCell = row.children[4]; if (syllabusCell) { this.addLinkToCell(syllabusCell, true); this.addDeleteButton(syllabusCell); } }); } catch (error) { console.error('Error adding links to timetable:', error); } } addLinkToCell(cell, isSyllabus) { if (cell.querySelector('.syllabus-link')) return; let courseCode = ''; if (isSyllabus) { const courseCodeCell = cell.parentElement.querySelector('td:nth-child(3)'); if (courseCodeCell) { courseCode = courseCodeCell.textContent.trim(); } } else { const courseCodeElement = cell.querySelector('td[valign="top"]'); if (courseCodeElement) { const textContent = courseCodeElement.textContent.trim(); const lines = textContent.split('\n'); courseCode = lines[0].trim(); } } if (!courseCode) return; const link = document.createElement('a'); link.className = 'syllabus-link'; link.textContent = 'シラバス'; link.href = `https://kdb.tsukuba.ac.jp/syllabi/${new Date().getFullYear()}/${courseCode}/jpn`; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.addEventListener('click', function (event) { event.preventDefault(); window.open(this.href, 'syllabusWindow', 'width=800,height=600,resizable=yes,scrollbars=yes'); }); const linkContainer = document.createElement('div'); linkContainer.classList.add('syllabus-link-container'); linkContainer.appendChild(link); if (!cell.querySelector('.syllabus-link-container')) { cell.appendChild(linkContainer); } } addDeleteButton(cell) { if (cell.querySelector('.delete-button')) return; const deleteButton = document.createElement('button'); deleteButton.classList.add('delete-button'); deleteButton.innerHTML = '✕'; deleteButton.addEventListener('click', () => { const deleteLink = cell.querySelector('a[onclick^="return DeleteCallA"]'); if (deleteLink) { const onclickArgs = deleteLink.getAttribute('onclick').match(/'(.*?)'/g); if (onclickArgs && onclickArgs.length === 5) { const [nendo, jikanwariShozokuCode, jikanwariCode, yobi, jigen] = onclickArgs.map(arg => arg.replace(/'/g, '')); const confirmDelete = window.confirm('本当にこの科目を削除しますか?'); if (confirmDelete) { this.keyObserver.executeCommand('delete', jikanwariShozokuCode, yobi, jigen, jikanwariCode); } } } }); const deleteButtonContainer = document.createElement('div'); deleteButtonContainer.classList.add('delete-button-container'); deleteButtonContainer.appendChild(deleteButton); cell.style.position = 'relative'; cell.appendChild(deleteButtonContainer); } } class kdb_Displayer { constructor(urls, keyObserver) { this.urls = urls; this.keyObserver = keyObserver; if (!this.keyObserver) { console.error('KeyObserver not initialized'); return; } this.displayElement = null; this.toggleButton = null; this.searchContainer = null; this.sortContainer = null; this.tableContainer = null; this.jsonData = null; this.indexedData = null; this.filteredData = null; this.dayOfWeekSelect = null; this.periodSelect = null; this.currentPage = 1; this.pageSize = 20; this.currentUrlIndex = 0; this.init(); } async init() { try { const targetIframe = await this.waitForTargetIframe(); if (targetIframe && this.keyObserver) { const jsonData = await this.fetchJsonData(this.urls[this.currentUrlIndex]); this.createUI(); this.displayJsonData(jsonData); } else { console.warn('KeyObserver not initialized or target iframe not found'); } } catch (error) { console.error('JSON Error', error); } } async waitForTargetIframe(maxRetries = 10, retryDelay = 1000) { let retries = 0; return new Promise((resolve, reject) => { const checkIframe = () => { const targetIframe = document.querySelector('iframe[src*="campussquare.do?_flowId=RSW0001000-flow"]'); if (targetIframe) { resolve(targetIframe); } else { retries++; if (retries < maxRetries) { setTimeout(checkIframe, retryDelay); } else { resolve(null); } } }; checkIframe(); }); } async fetchJsonData(url) { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error status: ${response.status}`); } return await response.json(); } createUI() { const existingElement = document.getElementById('kdb_Displayer'); const existingToggleButton = document.getElementById('jsonToggleButton'); if (existingElement) { this.displayElement = existingElement; this.toggleButton = existingToggleButton; this.searchContainer = document.getElementById('searchContainer'); this.sortContainer = document.getElementById('sortContainer'); this.tableContainer = document.getElementById('tableContainer'); } else { this.displayElement = document.createElement('div'); this.displayElement.id = 'kdb_Displayer'; this.displayElement.style.position = 'fixed'; this.displayElement.style.bottom = '2vh'; this.displayElement.style.right = '2vw'; this.displayElement.style.width = '40vw'; this.displayElement.style.maxHeight = '60vh'; this.displayElement.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'; this.displayElement.style.color = 'black'; this.displayElement.style.padding = '1vw'; this.displayElement.style.zIndex = '9999'; this.displayElement.style.overflowY = 'auto'; this.displayElement.style.border = '1px solid #ccc'; this.displayElement.style.borderRadius = '5px'; this.displayElement.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.1)'; this.displayElement.style.display = 'none'; // Header const headerDiv = document.createElement('div'); headerDiv.style.display = 'flex'; headerDiv.style.alignItems = 'center'; headerDiv.style.marginBottom = '10px'; const mainHeaderText = document.createElement('span'); mainHeaderText.style.fontWeight = 'bold'; mainHeaderText.style.fontSize = '25px'; mainHeaderText.textContent = 'KDB Searcher'; const subHeaderText = document.createElement('span'); subHeaderText.style.fontSize = '14px'; subHeaderText.style.marginLeft = '10px'; const subHeaderTextNode = document.createTextNode('Source code is available on '); subHeaderText.appendChild(subHeaderTextNode); const githubLink = document.createElement('a'); githubLink.href = 'https://github.com/refiaa'; githubLink.target = '_blank'; githubLink.textContent = 'Github'; subHeaderText.appendChild(githubLink); headerDiv.appendChild(mainHeaderText); headerDiv.appendChild(subHeaderText); this.displayElement.appendChild(headerDiv); this.toggleButton = document.createElement('button'); this.toggleButton.id = 'jsonToggleButton'; this.toggleButton.textContent = 'kdbを開く'; this.toggleButton.style.position = 'fixed'; this.toggleButton.style.bottom = '20px'; this.toggleButton.style.right = '20px'; this.toggleButton.style.padding = '5px 10px'; this.toggleButton.style.zIndex = '9999'; this.toggleButton.style.cursor = 'pointer'; this.toggleButton.addEventListener('click', this.toggleDisplay.bind(this)); this.searchContainer = document.createElement('div'); this.searchContainer.id = 'searchContainer'; this.sortContainer = document.createElement('div'); this.sortContainer.id = 'sortContainer'; this.tableContainer = document.createElement('div'); this.tableContainer.id = 'tableContainer'; this.createSearchUI(); this.createSortUI(); this.createUrlSwitchUI(); const spacerDiv1 = document.createElement('div'); spacerDiv1.style.marginBottom = '15px'; this.displayElement.appendChild(spacerDiv1); this.displayElement.appendChild(this.searchContainer); this.displayElement.appendChild(this.sortContainer); this.displayElement.appendChild(this.tableContainer); // レファレンス const descriptionDiv = document.createElement('div'); descriptionDiv.style.marginTop = '20px'; descriptionDiv.style.fontSize = '14px'; descriptionDiv.style.color = '#666'; const descriptionTextNode1 = document.createTextNode('Using '); descriptionDiv.appendChild(descriptionTextNode1); const kdbLink = document.createElement('a'); kdbLink.href = 'https://github.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb'; kdbLink.target = '_blank'; kdbLink.textContent = 'alternative-tsukuba-kdb'; descriptionDiv.appendChild(kdbLink); const descriptionTextNode2 = document.createTextNode(' for kdb data.'); descriptionDiv.appendChild(descriptionTextNode2); this.displayElement.appendChild(descriptionDiv); document.body.appendChild(this.displayElement); document.body.appendChild(this.toggleButton); } } createSearchUI() { const searchUIContainer = document.createElement('div'); searchUIContainer.style.marginBottom = '20px'; const subjectSearchRow = document.createElement('div'); subjectSearchRow.classList.add('row') const subjectCodeInput = document.createElement('input'); subjectCodeInput.type = 'text'; subjectCodeInput.placeholder = '科目番号で検索'; subjectCodeInput.style.marginRight = '10px'; subjectCodeInput.addEventListener('input', this.filterData.bind(this)); subjectSearchRow.appendChild(subjectCodeInput); const subjectNameInput = document.createElement('input'); subjectNameInput.type = 'text'; subjectNameInput.placeholder = '科目名で検索'; subjectNameInput.style.marginRight = '10px'; subjectNameInput.addEventListener('input', this.filterData.bind(this)); subjectSearchRow.appendChild(subjectNameInput); this.searchContainer.appendChild(subjectSearchRow); const dayTimeRow = document.createElement('div'); dayTimeRow.classList.add('row'); const daysOfWeekOptions = ['月', '火', '水', '木', '金', '土', '日']; this.dayOfWeekSelect = document.createElement('select'); const daysOfWeekPlaceholderOption = document.createElement('option'); daysOfWeekPlaceholderOption.value = ''; daysOfWeekPlaceholderOption.textContent = '曜日'; daysOfWeekPlaceholderOption.disabled = true; daysOfWeekPlaceholderOption.selected = true; this.dayOfWeekSelect.appendChild(daysOfWeekPlaceholderOption); for (const option of daysOfWeekOptions) { const daysOfWeekOption = document.createElement('option'); daysOfWeekOption.value = option; daysOfWeekOption.textContent = option; this.dayOfWeekSelect.appendChild(daysOfWeekOption); } this.dayOfWeekSelect.addEventListener('change', this.filterData.bind(this)); dayTimeRow.appendChild(this.dayOfWeekSelect); const periodsOptions = ['1', '2', '3', '4', '5', '6', '7', '8']; this.periodSelect = document.createElement('select'); const periodPlaceholderOption = document.createElement('option'); periodPlaceholderOption.value = ''; periodPlaceholderOption.textContent = '時限'; periodPlaceholderOption.disabled = true; periodPlaceholderOption.selected = true; this.periodSelect.appendChild(periodPlaceholderOption); for (const option of periodsOptions) { const periodsOption = document.createElement('option'); periodsOption.value = option; periodsOption.textContent = option; this.periodSelect.appendChild(periodsOption); } this.periodSelect.addEventListener('change', this.filterData.bind(this)); dayTimeRow.appendChild(this.periodSelect); this.searchContainer.appendChild(dayTimeRow); const filterRow = document.createElement('div'); filterRow.classList.add('row'); const semesterContainer = document.createElement('div'); const semesterOptions = ['春', '秋', 'A', 'B', 'C']; for (const option of semesterOptions) { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = option; checkbox.id = `semester-${option}`; checkbox.style.marginRight = '5px'; checkbox.addEventListener('change', this.filterData.bind(this)); semesterContainer.appendChild(checkbox); const label = document.createElement('label'); label.htmlFor = `semester-${option}`; label.textContent = option; semesterContainer.appendChild(label); } filterRow.appendChild(semesterContainer); const onlineOfflineContainer = document.createElement('div'); const onlineOfflineOptions = ['オンライン', '対面']; for (const option of onlineOfflineOptions) { const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = option; checkbox.id = `format-${option}`; checkbox.style.marginRight = '5px'; checkbox.addEventListener('change', this.filterData.bind(this)); onlineOfflineContainer.appendChild(checkbox); const label = document.createElement('label'); label.htmlFor = `format-${option}`; label.textContent = option; onlineOfflineContainer.appendChild(label); } filterRow.appendChild(onlineOfflineContainer); const resetButton = document.createElement('button'); resetButton.textContent = '検索結果をリセット'; resetButton.style.marginLeft = '80px'; resetButton.addEventListener('click', this.resetSearchOptions.bind(this)); onlineOfflineContainer.appendChild(resetButton); this.searchContainer.appendChild(filterRow); const style = document.createElement('style'); style.textContent = `.row {margin-bottom: 10px;}`; document.head.appendChild(style); } resetSearchOptions() { const subjectCodeInput = this.displayElement.querySelector('input[placeholder="科目番号で検索"]'); const subjectNameInput = this.displayElement.querySelector('input[placeholder="科目名で検索"]'); const semesterCheckboxes = this.displayElement.querySelectorAll('input[id^="semester-"]'); const onlineOfflineCheckboxes = this.displayElement.querySelectorAll('input[id^="format-"]'); subjectCodeInput.value = ''; subjectNameInput.value = ''; this.dayOfWeekSelect.selectedIndex = 0; this.periodSelect.selectedIndex = 0; semesterCheckboxes.forEach(checkbox => checkbox.checked = false); onlineOfflineCheckboxes.forEach(checkbox => checkbox.checked = false); this.filterData(); } createSortUI() { const sortLabel = document.createElement('label'); sortLabel.textContent = '並び変え: '; this.sortContainer.appendChild(sortLabel); const sortSelect = document.createElement('select'); sortSelect.style.marginRight = '10px'; const sortOptions = [ {value: 'subjectCode', label: '科目番号'}, {value: 'subjectName', label: '科目名'}, ]; for (const option of sortOptions) { const sortOption = document.createElement('option'); sortOption.value = option.value; sortOption.textContent = option.label; sortSelect.appendChild(sortOption); } sortSelect.addEventListener('change', this.sortData.bind(this)); this.sortContainer.appendChild(sortSelect); const sortOrderSelect = document.createElement('select'); const sortOrderOptions = [ {value: 'asc', label: '昇順'}, {value: 'desc', label: '降順'}, ]; for (const option of sortOrderOptions) { const sortOrderOption = document.createElement('option'); sortOrderOption.value = option.value; sortOrderOption.textContent = option.label; sortOrderSelect.appendChild(sortOrderOption); } sortOrderSelect.addEventListener('change', this.sortData.bind(this)); this.sortContainer.style.marginBottom = '15px' this.sortContainer.appendChild(sortOrderSelect); } toggleDisplay() { if (this.displayElement.style.display === 'none') { this.displayElement.style.display = 'block'; } else { this.displayElement.style.display = 'none'; } } createUrlSwitchUI() { const urlSwitchContainer = document.createElement('div'); urlSwitchContainer.style.marginTop = '10px'; urlSwitchContainer.style.display = 'flex'; urlSwitchContainer.style.alignItems = 'center'; const toggleSwitch = document.createElement('div'); toggleSwitch.style.position = 'relative'; toggleSwitch.style.display = 'inline-block'; toggleSwitch.style.width = '40px'; toggleSwitch.style.height = '20px'; toggleSwitch.style.borderRadius = '20px'; toggleSwitch.style.backgroundColor = '#ccc'; toggleSwitch.style.cursor = 'pointer'; toggleSwitch.style.transition = 'background-color 0.3s'; const toggleIndicator = document.createElement('div'); toggleIndicator.style.position = 'absolute'; toggleIndicator.style.top = '2px'; toggleIndicator.style.left = '2px'; toggleIndicator.style.width = '16px'; toggleIndicator.style.height = '16px'; toggleIndicator.style.borderRadius = '50%'; toggleIndicator.style.backgroundColor = 'white'; toggleIndicator.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.2)'; toggleIndicator.style.transition = 'transform 0.3s'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.style.display = 'none'; checkbox.addEventListener('change', () => { this.currentUrlIndex = checkbox.checked ? 1 : 0; this.resetSearchOptions(); this.fetchJsonData(this.urls[this.currentUrlIndex]).then((jsonData) => { this.displayJsonData(jsonData); }); toggleIndicator.style.transform = checkbox.checked ? 'translateX(20px)' : 'translateX(0)'; toggleSwitch.style.backgroundColor = checkbox.checked ? '#2196F3' : '#ccc'; }); toggleSwitch.appendChild(checkbox); toggleSwitch.appendChild(toggleIndicator); const labelUrl1 = document.createElement('span'); labelUrl1.textContent = '学類'; labelUrl1.style.marginRight = '5px'; labelUrl1.style.fontSize = '14px'; const labelUrl2 = document.createElement('span'); labelUrl2.textContent = '大学院'; labelUrl2.style.marginLeft = '5px'; labelUrl2.style.fontSize = '14px'; urlSwitchContainer.appendChild(labelUrl1); urlSwitchContainer.appendChild(toggleSwitch); urlSwitchContainer.appendChild(labelUrl2); toggleSwitch.addEventListener('click', () => { checkbox.checked = !checkbox.checked; checkbox.dispatchEvent(new Event('change')); }); this.displayElement.appendChild(urlSwitchContainer); } async displayJsonData(jsonData) { this.jsonData = jsonData; this.indexData(jsonData.subject); this.filteredData = jsonData.subject; this.renderTable(); } indexData(data) { this.indexedData = { subjectCode: {}, subjectName: {}, semester: {}, format: {}, dayOfWeek: {}, period: {}, }; data.forEach((subject, index) => { const subjectCode = subject[0]; const subjectName = subject[1]; const semester = subject[5]; const format = subject[10]; const timetable = subject[6]; const dayOfWeek = timetable.slice(0, 1); const period = timetable.slice(1); if (!this.indexedData.subjectCode[subjectCode]) { this.indexedData.subjectCode[subjectCode] = []; } this.indexedData.subjectCode[subjectCode].push(index); if (!this.indexedData.subjectName[subjectName]) { this.indexedData.subjectName[subjectName] = []; } this.indexedData.subjectName[subjectName].push(index); if (!this.indexedData.semester[semester]) { this.indexedData.semester[semester] = []; } this.indexedData.semester[semester].push(index); if (!this.indexedData.format[format]) { this.indexedData.format[format] = []; } this.indexedData.format[format].push(index); if (!this.indexedData.dayOfWeek[dayOfWeek]) { this.indexedData.dayOfWeek[dayOfWeek] = []; } this.indexedData.dayOfWeek[dayOfWeek].push(index); if (!this.indexedData.period[period]) { this.indexedData.period[period] = []; } this.indexedData.period[period].push(index); }); } renderTable() { const tableHeaders = [' ', '科目番号', '科目名', '単位', '年次', '開講時期', '時間割', '教室', '担当教員', '概要', '備考']; let tableHtml = ` <table style="border-collapse: collapse; width: 100%;"> <thead> <tr style="background-color: #f2f2f2;"> ${tableHeaders.map(header => `<th style="border: 1px solid #ddd; padding: 12px; text-align: left;">${header}</th>`).join('')} </tr> </thead> <tbody> `; const startIndex = (this.currentPage - 1) * this.pageSize; const endIndex = startIndex + this.pageSize; const paginatedData = this.filteredData.slice(startIndex, endIndex); if (paginatedData.length === 0) { tableHtml += ` <tr> <td colspan="${tableHeaders.length}" style="text-align: center; padding: 8px;">検索結果がありません</td> </tr> `; } else { paginatedData.forEach((subject, index) => { const rowStyle = index % 2 === 0 ? 'background-color: #f9f9f9;' : ''; tableHtml += ` <tr style="${rowStyle}"> <td style="border: 1px solid #ddd; padding: 8px;"> <button class="subject-button syllabus" data-subject-code="${subject[0]}" style="width: 80px;">シラバス</button> <button class="subject-button add" data-subject-code="${subject[0]}" style="width: 80px;">科目を追加</button> </td> <td style="border: 1px solid #ddd; padding: 8px;">${subject[0]}</td> <td style="border: 1px solid #ddd; padding: 8px;">${subject[1]}</td> <td style="border: 1px solid #ddd; padding: 8px;">${subject[3]}</td> <td style="border: 1px solid #ddd; padding: 8px;">${subject[4]}</td> <td style="border: 1px solid #ddd; padding: 8px;">${subject[5]}</td> <td style="border: 1px solid #ddd; padding: 8px;">${subject[6]}</td> <td style="border: 1px solid #ddd; padding: 8px;">${subject[7]}</td> <td style="border: 1px solid #ddd; padding: 8px;">${subject[8]}</td> <td style="border: 1px solid #ddd; padding: 8px;">${subject[9]}</td> <td style="border: 1px solid #ddd; padding: 8px;">${subject[10]}</td> </tr> `; }); } tableHtml += ` </tbody> </table> `; const totalPages = Math.ceil(this.filteredData.length / this.pageSize); const paginationHtml = ` <div style="margin-top: 10px;"> <button id="prevPage" style="margin-right: 5px;">前</button> <span>ページ ${this.currentPage} / ${totalPages}</span> <button id="nextPage" style="margin-left: 5px;">次</button> </div> `; this.tableContainer.innerHTML = tableHtml + paginationHtml; const prevPageButton = this.tableContainer.querySelector('#prevPage'); const nextPageButton = this.tableContainer.querySelector('#nextPage'); prevPageButton.addEventListener('click', () => { if (this.currentPage > 1) { this.currentPage--; this.renderTable(); } }); nextPageButton.addEventListener('click', () => { if (this.currentPage < totalPages) { this.currentPage++; this.renderTable(); } }); this.addSubjectButtonListeners(); } addSubjectButtonListeners() { const addButtons = this.tableContainer.querySelectorAll('.subject-button.add'); addButtons.forEach((button) => { button.addEventListener('click', () => { const subjectCode = button.dataset.subjectCode; const subjectName = button.closest('tr').querySelector('td:nth-child(3)').textContent.trim(); const confirmAdd = window.confirm(`科目名 ${subjectName} (科目番号 : ${subjectCode}) を追加しますか?`); if (confirmAdd) { this.handleSubjectButtonClick(subjectCode, subjectName, 'add'); } }); }); const syllabusButtons = this.tableContainer.querySelectorAll('.subject-button.syllabus'); syllabusButtons.forEach((button) => { button.addEventListener('click', () => { const subjectCode = button.dataset.subjectCode; this.handleSubjectButtonClick(subjectCode, '', 'syllabus'); }); }); } handleSubjectButtonClick(subjectCode, subjectName, action) { if (!this.keyObserver) { console.error('KeyObserver or its required properties are not initialized'); return; } if (action === 'add') { try { this.keyObserver.inputField.value = subjectCode; this.keyObserver.addButton.click(); } catch (error) { console.error('Error adding subject:', error); alert('科目の追加中にエラーが発生しました。'); } } else if (action === 'syllabus') { const syllabusUrl = `https://kdb.tsukuba.ac.jp/syllabi/${new Date().getFullYear()}/${subjectCode}/jpn`; window.open(syllabusUrl, '_blank', 'width=800,height=600,resizable=yes,scrollbars=yes'); } } filterData() { const subjectCodeInput = this.displayElement.querySelector('input[placeholder="科目番号で検索"]'); const subjectNameInput = this.displayElement.querySelector('input[placeholder="科目名で検索"]'); const semesterCheckboxes = this.displayElement.querySelectorAll('input[id^="semester-"]:checked'); const onlineOfflineCheckboxes = this.displayElement.querySelectorAll('input[id^="format-"]:checked'); const subjectCode = subjectCodeInput.value.toLowerCase(); const subjectName = subjectNameInput.value.toLowerCase(); const selectedDayOfWeek = this.dayOfWeekSelect.value; const selectedPeriod = this.periodSelect.value; const selectedSemesters = Array.from(semesterCheckboxes).map(checkbox => checkbox.value); const selectedFormats = Array.from(onlineOfflineCheckboxes).map(checkbox => checkbox.value); let filteredIndices = this.getIntersection( this.searchIndex(this.indexedData.subjectCode, subjectCode), this.searchIndex(this.indexedData.subjectName, subjectName), this.filterIndexByIncludingOptions(this.indexedData.dayOfWeek, [selectedDayOfWeek]), this.filterIndexByIncludingOptions(this.indexedData.period, [selectedPeriod]), this.filterIndexBySemesterModules(this.indexedData.semester, selectedSemesters), this.filterIndexByIncludingOptions(this.indexedData.format, selectedFormats) ); this.filteredData = filteredIndices.map(index => this.jsonData.subject[index]); this.currentPage = 1; this.renderTable(); } searchIndex(index, query) { if (query === '') { return Object.values(index).flat(); } const matches = Object.entries(index).filter(([key]) => key.toLowerCase().includes(query)); return matches.map(([_, indices]) => indices).flat(); } /** * @param {Object} index - 検索対象のindex * @param {string[]} selectedOptions - 選択したoptionsの配列 * @returns {number[]} - 選択したoptionsを含む項目のindex配列 */ filterIndexByIncludingOptions(index, selectedOptions) { if (selectedOptions.length === 0) { return Object.values(index).flat(); } const matches = Object.entries(index).filter(([key, indices]) => selectedOptions.some(option => key.includes(option))); return matches.map(([_, indices]) => indices).flat(); } filterIndexBySemesterModules(index, selectedSemesters) { if (selectedSemesters.length === 0) { return Object.values(index).flat(); } const matches = Object.entries(index).filter(([key, indices]) => { return selectedSemesters.every(semester => key.includes(semester)); }); return matches.map(([_, indices]) => indices).flat(); } getIntersection(...arrays) { return arrays.reduce((a, b) => a.filter(c => b.includes(c))); } sortData() { const sortSelect = this.displayElement.querySelector('select'); const sortOrderSelect = sortSelect.nextElementSibling; const sortBy = sortSelect.value; const sortOrder = sortOrderSelect.value; const subjectIndex = sortBy === 'subjectCode' ? 0 : 1; this.filteredData.sort((a, b) => { const subjectA = a[subjectIndex].toLowerCase(); const subjectB = b[subjectIndex].toLowerCase(); if (subjectA < subjectB) return sortOrder === 'asc' ? -1 : 1; if (subjectA > subjectB) return sortOrder === 'asc' ? 1 : -1; return 0; }); this.currentPage = 1; this.renderTable(); } } class LogAnalyzer { constructor() { this.logKey = 'flowExecutionLogs'; } getLogs() { const logs = sessionStorage.getItem(this.logKey); return logs ? JSON.parse(logs) : []; } sortLogsByTimestamp(logs) { return logs.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); } cleanUpLogs() { const logs = this.getLogs(); const maxLogs = 10; const minLogsToKeep = 5; if (logs.length > maxLogs) { const sortedLogs = this.sortLogsByTimestamp(logs); const logsToKeep = sortedLogs.slice(-minLogsToKeep); sessionStorage.setItem(this.logKey, JSON.stringify(logsToKeep)); } } detectAbnormalPattern(logs) { const sortedLogs = this.sortLogsByTimestamp(logs); const validPattern = sortedLogs.slice(-3); const patternTypes = validPattern.map(log => log.type).join('-'); return patternTypes === 'input-insert-back'; } analyzeLogs() { this.cleanUpLogs(); const logs = this.getLogs(); const isAbnormal = this.detectAbnormalPattern(logs); if (isAbnormal) { alert("科目が追加されませんでした。時限・曜日を確認してください。"); } } } // kdbっぽいなにか https://github.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb から取ってきています。 const jsonUrls = [ 'https://raw.githubusercontent.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb/main/src/kdb.json', // 学類 'https://raw.githubusercontent.com/Make-IT-TSUKUBA/alternative-tsukuba-kdb/main/src/kdb-grad.json' // 大学院 ]; new kdb_Displayer(jsonUrls); let keyObserver = null; let advancedSyllabus = null; function initializeEnhancer() { const targetIframe = document.querySelector('iframe[src*="campussquare.do?_flowId=RSW0001000-flow"]'); if (targetIframe) { targetIframe.addEventListener('load', function () { try { const iframeDocument = targetIframe.contentDocument || targetIframe.contentWindow.document; if (iframeDocument.readyState === 'complete') { keyObserver = new KeyObserver('body'); advancedSyllabus = new AdvancedSyllabus(keyObserver); new kdb_Displayer(jsonUrls, keyObserver); } else { setTimeout(initializeEnhancer, 100); } } catch (error) { console.error('Error accessing iframe content:', error); setTimeout(initializeEnhancer, 1000); } }); } else { console.warn('Target iframe not found, retrying in 1 second'); setTimeout(initializeEnhancer, 1000); } } function observePageChanges() { const observerOptions = { childList: true, subtree: true }; const observer = new MutationObserver(function(mutationsList, observer) { for (const mutation of mutationsList) { if (mutation.type === 'childList') { const addedIframes = Array.from(mutation.addedNodes).filter(node => node.nodeType === Node.ELEMENT_NODE && node.tagName === 'IFRAME'); if (addedIframes.length > 0) { console.log('New iframe(s) added:', addedIframes); initializeEnhancer(); } } } }); observer.observe(document.body, observerOptions); } observePageChanges(); })();