Created
July 27, 2026 08:23
-
-
Save xkikeg/9f34153802d5aba989e2261fe7eae341 to your computer and use it in GitHub Desktop.
Swisspass to CSV Tampermonkey csript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Swisspass Halbtax Plus | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0.0 | |
| // @description Get CSV out of Halbtax Plus | |
| // @author kikeg | |
| // @match https://www.swisspass.ch/info/abos/detailRefilledContract/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=swisspass.ch | |
| // @grant GM.setClipboard | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const HEADING_TEXT = 'Saldoverlauf in CHF'; | |
| const BTN_ID = 'saldoverlauf-csv-btn'; | |
| // --- ユーティリティ --------------------------------------------------- | |
| const norm = (s) => (s || '').replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim(); | |
| /** 対象の h3 を探す */ | |
| function findHeading() { | |
| return [...document.querySelectorAll('h3')] | |
| .find((h) => norm(h.textContent) === HEADING_TEXT) || null; | |
| } | |
| /** h3 の後続の兄弟から table を探す */ | |
| function findTable(h3) { | |
| let el = h3.nextElementSibling; | |
| while (el) { | |
| if (el.tagName === 'TABLE') return el; | |
| const t = el.querySelector('table'); | |
| if (t) return t; | |
| // 次の見出しまで来たら打ち切り | |
| if (/^H[1-6]$/.test(el.tagName)) return null; | |
| el = el.nextElementSibling; | |
| } | |
| return null; | |
| } | |
| /** CSV の1セルをエスケープ */ | |
| function esc(v) { | |
| const s = norm(v); | |
| return /[",\n\r;]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; | |
| } | |
| /** table -> CSV文字列 */ | |
| function tableToCsv(table) { | |
| const rows = [...table.querySelectorAll('tr')]; | |
| return rows | |
| .map((tr) => | |
| [...tr.querySelectorAll('th,td')] | |
| .map((cell) => esc(cell.innerText ?? cell.textContent)) | |
| .join(',') | |
| ) | |
| .join('\r\n'); | |
| } | |
| /** GM.setClipboard / GM_setClipboard / navigator.clipboard の順で試す */ | |
| async function copyToClipboard(text) { | |
| if (typeof GM !== 'undefined' && GM && typeof GM.setClipboard === 'function') { | |
| await GM.setClipboard(text, 'text'); | |
| return; | |
| } | |
| if (typeof GM_setClipboard === 'function') { | |
| GM_setClipboard(text, 'text'); | |
| return; | |
| } | |
| // 最終フォールバック | |
| await navigator.clipboard.writeText(text); | |
| } | |
| // --- ボタン設置 ------------------------------------------------------- | |
| function insertButton(h3) { | |
| if (document.getElementById(BTN_ID)) return; | |
| const btn = document.createElement('button'); | |
| btn.id = BTN_ID; | |
| btn.type = 'button'; | |
| btn.textContent = 'CSVをコピー'; | |
| btn.style.cssText = | |
| 'margin:8px 0;padding:6px 14px;font-size:13px;cursor:pointer;' + | |
| 'border:1px solid #888;border-radius:4px;background:#f5f5f5;'; | |
| btn.addEventListener('click', async (e) => { | |
| e.preventDefault(); | |
| const table = findTable(h3); | |
| if (!table) { | |
| alert('テーブルが見つかりませんでした。読み込み完了後にもう一度お試しください。'); | |
| return; | |
| } | |
| try { | |
| await copyToClipboard(tableToCsv(table)); | |
| const orig = btn.textContent; | |
| btn.textContent = 'コピーしました ✓'; | |
| btn.disabled = true; | |
| setTimeout(() => { | |
| btn.textContent = orig; | |
| btn.disabled = false; | |
| }, 1500); | |
| } catch (err) { | |
| console.error(err); | |
| alert('クリップボードへのコピーに失敗しました: ' + err); | |
| } | |
| }); | |
| h3.insertAdjacentElement('afterend', btn); | |
| } | |
| // --- 出現監視(AJAX対応) -------------------------------------------- | |
| function tryInit() { | |
| const h3 = findHeading(); | |
| if (!h3) return false; | |
| // テーブルが出来上がってからボタンを出す | |
| if (!findTable(h3)) return false; | |
| insertButton(h3); | |
| return true; | |
| } | |
| // 1) まず即時チェック | |
| if (!tryInit()) { | |
| // 2) DOM変更を監視(AJAX描画・再描画の両方に対応) | |
| const observer = new MutationObserver(() => { | |
| tryInit(); // 見つかれば設置。消えたら再設置される。 | |
| }); | |
| observer.observe(document.documentElement, { childList: true, subtree: true }); | |
| // 3) 保険:一定間隔でもチェック(30秒で停止) | |
| const timer = setInterval(tryInit, 1000); | |
| setTimeout(() => clearInterval(timer), 30000); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment