「MediaWiki:Forum.js」の版間の差分
表示
編集の要約なし |
編集の要約なし |
||
| (同じ利用者による、間の10版が非表示) | |||
| 7行目: | 7行目: | ||
// 設定がない場合のデフォルト値 | // 設定がない場合のデフォルト値 | ||
mw.forum = mw.forum || {}; | mw.forum = mw.forum || {}; | ||
mw.forum.toppage = mw.forum.toppage || ' | mw.forum.toppage = mw.forum.toppage || 'KenryoBBS'; | ||
mw.forum.threads = mw.forum.threads || 20; | mw.forum.threads = mw.forum.threads || 20; | ||
mw.forum.sticky = mw.forum.sticky || []; | mw.forum.sticky = mw.forum.sticky || []; | ||
| 39行目: | 39行目: | ||
'.mw-editsection', // 節編集リンク | '.mw-editsection', // 節編集リンク | ||
'#t-permalink', // 念のため固定リンクも非表示 | '#t-permalink', // 念のため固定リンクも非表示 | ||
'#ca-history', // 「履歴を表示」タブ | |||
'#t-upload', // サイドバーのアップロードリンク | |||
].join(',') + '{display:none!important;}'; | ].join(',') + '{display:none!important;}'; | ||
document.head.appendChild(editBlockStyle); | document.head.appendChild(editBlockStyle); | ||
| 47行目: | 49行目: | ||
// 動物リスト | // 動物リスト | ||
const animals = [ | const animals = [ | ||
' | 'イヌ', 'ネコ', 'ハムスター', 'ウシ', 'ウマ', 'ヒツジ', 'ヤギ', 'ブタ', 'ニワトリ', 'アヒル', | ||
' | 'クマ', 'サル', 'ゴリラ', 'チンパンジー', 'オランウータン', 'マンドリル', | ||
' | 'チーター', 'ヒョウ', 'ジャガー', 'ピューマ', 'ハイエナ', 'ミーアキャット', | ||
' | 'シマウマ', 'ガゼル', 'インパラ', 'バッファロー', 'ヌー', 'アルパカ', 'リャマ', 'ラクダ', | ||
' | 'カンガルー', 'ワラビー', 'ウォンバット', 'カモノハシ', 'ハリモグラ', | ||
' | 'アザラシ', 'アシカ', 'セイウチ', 'オットセイ', 'ジュゴン', 'マナティ', 'ラッコ', | ||
'シャチ', 'ベルーガ', 'マッコウクジラ', 'ザトウクジラ', 'ジンベエザメ', 'ホオジロザメ', 'ハンマーヘッドシャーク', | |||
'エイ', 'マンボウ', 'タツノオトシゴ', 'ウツボ', 'クリオネ', 'クラゲ', 'サンゴ', 'イソギンチャク', | |||
'ワシ', 'トンビ', 'ハト', 'インコ', 'オウム', 'ブンチョウ', 'カナリア', | |||
'ダチョウ', 'エミュー', 'ヒクイドリ', 'キウィ', 'フラミンゴ', 'クジャク', 'ハクチョウ', | |||
'カワセミ', 'ハヤブサ', 'ミミズク', 'キツツキ', 'カモメ', 'ペリカン', | |||
'ワニ', 'ヘビ', 'トカゲ', 'カメレオン', 'イグアナ', 'ヤモリ', 'コモドドラゴン', | |||
'カエル', 'イモリ', 'サンショウウオ', 'ウーパールーパー', | |||
'アリクイ', 'アルマジロ', 'ヤマアラシ', 'スカンク', 'イタチ', 'テン', 'カワウソ', 'ビーバー', | |||
'ムササビ', 'モモンガ', 'フェレット', 'チンチラ', 'デグー', 'モルモット', | |||
'ドラゴン', 'ユニコーン', 'ペガサス', 'フェニックス', 'グリフォン', 'ケンタウロス', 'ケルベロス', | |||
'ヒドラ', 'キマイラ', 'バジリスク', 'コカトリス', 'ワイバーン', 'マンティコア', 'ゴーレム', | |||
'クラーケン', 'リヴァイアサン', 'シーサーペント', '人魚', 'セイレーン', | |||
'カッパ', '天狗', '鬼', '龍', '麒麟', '鳳凰', '霊亀', '八咫烏', | |||
'ツチノコ', 'ネッシー', 'イエティ', 'ビッグフット', 'チュパカブラ', 'モンゴリアンデスワーム', | |||
'カブトムシ', 'クワガタ', 'チョウ', 'トンボ', 'セミ', 'カマキリ', 'ハチ', 'アリ', | |||
'クモ', 'サソリ', 'ムカデ', 'ダンゴムシ', 'ペンギン', 'イルカ', 'コアラ', 'パンダ', 'レッサーパンダ', | |||
'カピバラ', 'ナマケモノ', 'オポッサム', 'ミツバチ', 'テントウムシ', 'ホタル', 'カブトガニ', 'ダイオウグソクムシ', | |||
'シーラカンス', 'オウムガイ', 'アノマロカリス', 'ウミサソリ', 'トリロバイト', 'ディメトロドン', 'エダフォサウルス', | |||
'プレシオサウルス', 'モササウルス', 'ティラノサウルス', 'トリケラトプス', 'ステゴサウルス', 'ブラキオサウルス', | |||
'アンキロサウルス', 'ディプロドクス', 'ヴェロキラプトル', 'ギガノトサウルス', 'カルノタウルス', 'メガロサウルス', | |||
'スピノサウルス', 'イグアノドン', 'パラサウロロフス', 'エドモントサウルス', 'ヒプシロフォドン', 'マイアサウラ', | |||
'プロトケラトプス', 'オヴィラプトル', 'オルニトミムス', 'シノサウルス', 'サウロロフス', 'サウルハドン', 'ガリミムス', | |||
'アルバートサウルス', 'ダコタラプトル', 'アクロカントサウルス', 'シェノサウルス', 'エラスモサウルス', 'イクチオサウルス', | |||
'オルニトレステス', 'テリジノサウルス', 'アマルガサウルス', 'シンラプトル' | |||
]; | ]; | ||
| 146行目: | 172行目: | ||
// 匿名ユーザーのみ投稿フォームの上部に表示される | // 匿名ユーザーのみ投稿フォームの上部に表示される | ||
const anonSettingsHtml = isAnon ? `<details id="f-name-settings" style="margin-bottom:.8em;border:1px solid #ccc;padding:.5em;border-radius:3px;"> | const anonSettingsHtml = isAnon ? `<details id="f-name-settings" style="margin-bottom:.8em;border:1px solid #ccc;padding:.5em;border-radius:3px;"> | ||
<summary style="cursor:pointer;font-weight:bold;user-select:none;"> | <summary style="cursor:pointer;font-weight:bold;user-select:none;">名前・トリップの設定</summary> | ||
<div style="margin-top:.5em;"> | <div style="margin-top:.5em;"> | ||
<p style="margin:.2em 0 .5em;font-size:.9em;color:#555;"> | <p style="margin:.2em 0 .5em;font-size:.9em;color:#555;"> | ||
| 168行目: | 194行目: | ||
// ========== 書式ツールバーHTML ========== | // ========== 書式ツールバーHTML ========== | ||
const toolbarHtml = `<div id="f-toolbar" style="margin-bottom:.4em;display:flex;gap:.3em;flex-wrap:wrap;align-items:center;padding:.3em;background:#f8f8f8;border:1px solid #ccc;border-radius:3px;"> | const toolbarHtml = `<div id="f-toolbar" style="margin-bottom:.4em;display:flex;gap:.3em;flex-wrap:wrap;align-items:center;padding:.3em;background:#f8f8f8;border:1px solid #ccc;border-radius:3px;"> | ||
<span style="font-size:.85em;color:#555;margin-left:.3em;">文字色:</span> | <span style="font-size:.85em;color:#555;margin-left:.3em;">文字色:</span> | ||
${['#cc0000','#e07000','#007700','#0055cc','#7700aa','#555555'].map(c => | ${['#cc0000', '#e07000', '#007700', '#0055cc', '#7700aa', '#555555'].map(c => | ||
`<span class="f-tb-color" data-color="${c}" title="${c}" style="display:inline-block;width:1.2em;height:1.2em;background:${c};border:2px solid #aaa;border-radius:2px;cursor:pointer;vertical-align:middle;"></span>` | |||
).join('')} | ).join('')} | ||
</div>`; | </div>`; | ||
// ========================================= | // ========================================= | ||
| 185行目: | 209行目: | ||
deletesummary: 'delete post', | deletesummary: 'delete post', | ||
deletethreadsummary: 'スレッドの削除', | deletethreadsummary: 'スレッドの削除', | ||
toppage_css: "<style>.f-sticky>td:first-child>a::before{content:'';background-image:url(https://upload.wikimedia.org/wikipedia/commons/a/a5/OOjs_UI_icon_pushPin.svg);width:.8em;height:.8em;margin-right:.2em;display:inline-block;background-size:.8em}#f-loadmore{display:block;margin-left:auto;margin-right:auto;}</style>", | toppage_css: "<style>.f-sticky>td:first-child>a::before{content:'';background-image:url(https://upload.wikimedia.org/wikipedia/commons/a/a5/OOjs_UI_icon_pushPin.svg);width:.8em;height:.8em;margin-right:.2em;display:inline-block;background-size:.8em}#f-loadmore{display:block;margin-left:auto;margin-right:auto;}#f-create-btn-mobile{display:none;margin:1em 0;text-align:right;}@media (max-width:768px){.mw-indicators{display:block!important;}#f-create-btn-mobile{display:block;}}#f-notice{background:#fffacd;border:1px solid #daa520;border-radius:4px;padding:1em;margin:1em 0;font-size:.95em;line-height:1.6;}</style>", | ||
toppage_notice: mw.forum.notice ? `<div id="f-notice">${mw.forum.notice}</div>` : '', | |||
load_more: '<input type="button" value="もっと読み込む" class="mw-ui-button" id="f-loadmore">', | load_more: '<input type="button" value="もっと読み込む" class="mw-ui-button" id="f-loadmore">', | ||
create: '<input type="button" class="mw-ui-button mw-ui-progressive" value="スレッドを作成">', | create: '<input type="button" class="mw-ui-button mw-ui-progressive" value="スレッドを作成">', | ||
| 241行目: | 266行目: | ||
const source = await (await fetch(mw.config.get('wgScript') + `?title=${mw.config.get('wgPageName')}&action=raw`)).text(); | const source = await (await fetch(mw.config.get('wgScript') + `?title=${mw.config.get('wgPageName')}&action=raw`)).text(); | ||
const newText = source.replace(new RegExp(`(\\{\\{post\\|(.*?)\\|${postId}\\|(.*?)\\|4=)((.|\n)*?)(}})`), '$1[Deleted]$6'); | const newText = source.replace(new RegExp(`(\\{\\{post\\|(.*?)\\|${postId}\\|(.*?)\\|4=)((.|\n)*?)(}})`), '$1[Deleted]$6'); | ||
fetch('/ | fetch('/wiki/forum-proxy.php', { | ||
method: 'POST', | method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | headers: { 'Content-Type': 'application/json' }, | ||
body: JSON.stringify({ | body: JSON.stringify({ | ||
title: | title: mw.config.get('wgPageName'), | ||
text: | text: newText, | ||
summary: `${msg.deletesummary} #${postId}` | summary: `${msg.deletesummary} #${postId}` | ||
}) | }) | ||
| 263行目: | 288行目: | ||
const threadName = mw.config.get('wgPageName').replace(mw.forum.toppage + '/', ''); | const threadName = mw.config.get('wgPageName').replace(mw.forum.toppage + '/', ''); | ||
if (!confirm(`スレッド「${threadName}」を削除しますか?\nこの操作は取り消せません。`)) return; | if (!confirm(`スレッド「${threadName}」を削除しますか?\nこの操作は取り消せません。`)) return; | ||
fetch('/ | fetch('/wiki/forum-proxy.php', { | ||
method: 'POST', | method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | headers: { 'Content-Type': 'application/json' }, | ||
body: JSON.stringify({ | body: JSON.stringify({ | ||
action: 'delete', | action: 'delete', | ||
title: | title: mw.config.get('wgPageName'), | ||
reason: msg.deletethreadsummary | reason: msg.deletethreadsummary | ||
}) | }) | ||
| 293行目: | 318行目: | ||
ta.value = ta.value.substring(0, s) + start + selected + end + ta.value.substring(e); | ta.value = ta.value.substring(0, s) + start + selected + end + ta.value.substring(e); | ||
ta.selectionStart = s + start.length; | ta.selectionStart = s + start.length; | ||
ta.selectionEnd | ta.selectionEnd = s + start.length + selected.length; | ||
ta.focus(); | ta.focus(); | ||
}; | }; | ||
| 375行目: | 400行目: | ||
const anonParam = isAnon ? '|anon=1' : ''; | const anonParam = isAnon ? '|anon=1' : ''; | ||
content += `{{post|${mw.forum.username}|1|{{subst:#timel:Y/m/d H:i:s}}|4=${document.querySelector('#wpTextbox1').value}${anonParam}}}`; | content += `{{post|${mw.forum.username}|1|{{subst:#timel:Y/m/d H:i:s}}|4=${document.querySelector('#wpTextbox1').value}${anonParam}}}`; | ||
fetch('/ | fetch('/wiki/forum-proxy.php', { | ||
method: 'POST', | method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | headers: { 'Content-Type': 'application/json' }, | ||
| 398行目: | 423行目: | ||
mc.innerHTML = msg.loading; | mc.innerHTML = msg.loading; | ||
func.getthreads().then((res) => { | func.getthreads().then((res) => { | ||
mc.innerHTML = res[0] + msg.toppage_css; | mc.innerHTML = msg.toppage_notice + res[0] + msg.toppage_css; | ||
// モバイル用の「スレッドを作成」ボタンを追加 | |||
const mobileBtn = document.createElement('div'); | |||
mobileBtn.id = 'f-create-btn-mobile'; | |||
mobileBtn.innerHTML = msg.create; | |||
mc.insertBefore(mobileBtn, mc.firstChild); | |||
mobileBtn.querySelector('input').onclick = function () { | |||
const url = new URL(window.location.href); | |||
url.searchParams.set('newthread', '1'); | |||
location.href = url; | |||
}; | |||
if (res[1]) { | if (res[1]) { | ||
document.querySelector('#mw-content-text').innerHTML += msg.load_more; | document.querySelector('#mw-content-text').innerHTML += msg.load_more; | ||
| 449行目: | 486行目: | ||
document.querySelector('#f-post').disabled = true; | document.querySelector('#f-post').disabled = true; | ||
const source = (await (await fetch(mw.config.get('wgScript') + `?title=${mw.config.get('wgPageName')}&action=raw`)).text()); | const source = (await (await fetch(mw.config.get('wgScript') + `?title=${mw.config.get('wgPageName')}&action=raw`)).text()); | ||
const lp = source.split('{{post|').length; | const lp = source.split('{{post|').length - 1; // 0スレッドを考慮して投稿数+1をIDにする | ||
let summary; | let summary; | ||
if (fedit) { | if (fedit) { | ||
| 465行目: | 502行目: | ||
// プロキシ経由でBot名義に編集する | // プロキシ経由でBot名義に編集する | ||
const postBody = { | const postBody = { | ||
title: | title: mw.config.get('wgPageName'), | ||
summary: summary | summary: summary | ||
}; | }; | ||
| 473行目: | 510行目: | ||
postBody.appendtext = `\n{{post|${mw.forum.username}|${lp}|{{subst:#timel:Y/m/d H:i:s}}|4=${document.querySelector('#wpTextbox1').value}${document.querySelector('#f-reply-cb').checked ? '|re=' + document.querySelector('#f-reply').value : ''}${anonParam}}}`; | postBody.appendtext = `\n{{post|${mw.forum.username}|${lp}|{{subst:#timel:Y/m/d H:i:s}}|4=${document.querySelector('#wpTextbox1').value}${document.querySelector('#f-reply-cb').checked ? '|re=' + document.querySelector('#f-reply').value : ''}${anonParam}}}`; | ||
} | } | ||
fetch('/ | fetch('/wiki/forum-proxy.php', { | ||
method: 'POST', | method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | headers: { 'Content-Type': 'application/json' }, | ||
| 524行目: | 561行目: | ||
}); | }); | ||
} | } | ||
// ========== レスホバープレビュー ========== | |||
// >>>N のリンクにマウスを乗せると該当投稿をポップアップ表示する | |||
(() => { | |||
// ポップアップ用の浮動div(ページに1つだけ作成) | |||
const popup = document.createElement('div'); | |||
popup.id = 'f-hover-popup'; | |||
popup.style.cssText = [ | |||
'position:fixed', | |||
'z-index:9999', | |||
'max-width:480px', | |||
'min-width:200px', | |||
'background:#fff', | |||
'border:2px solid #BBBBBB', | |||
'border-radius:3px', | |||
'box-shadow:2px 4px 12px rgba(0,0,0,.2)', | |||
'pointer-events:none', // ポップアップ自体はマウスイベントを無視 | |||
'display:none', | |||
'font-size:.9em', | |||
'line-height:1.4', | |||
].join(';'); | |||
document.body.appendChild(popup); | |||
let hideTimer = null; | |||
const showPopup = (anchorEl, postId) => { | |||
const target = document.querySelector(`#post-${postId}`); | |||
if (!target) return; | |||
// 対象投稿をクローンしてポップアップに表示 | |||
const clone = target.cloneNode(true); | |||
clone.querySelectorAll('a[href*="fedit"], span[style*="#d33"]').forEach(el => el.remove()); | |||
popup.innerHTML = ''; | |||
popup.appendChild(clone); | |||
// ポップアップの位置をリンクの近くに配置(モバイル対応) | |||
const rect = anchorEl.getBoundingClientRect(); | |||
const viewportWidth = window.innerWidth; | |||
const viewportHeight = window.innerHeight; | |||
// ポップアップの最大幅を画面幅の90%に制限 | |||
const maxWidth = Math.min(480, viewportWidth * 0.9); | |||
popup.style.maxWidth = maxWidth + 'px'; | |||
// 一度表示して実際の高さを取得 | |||
popup.style.display = 'block'; | |||
popup.style.visibility = 'hidden'; | |||
const popupHeight = popup.offsetHeight; | |||
popup.style.visibility = 'visible'; | |||
// 縦位置:下に余裕があれば下、なければ上 | |||
const spaceBelow = viewportHeight - rect.bottom; | |||
const top = spaceBelow > popupHeight + 10 | |||
? rect.bottom + 4 | |||
: Math.max(4, rect.top - popupHeight - 4); | |||
// 横位置:画面からはみ出ないように調整 | |||
let left = rect.left; | |||
if (left + maxWidth > viewportWidth - 10) { | |||
left = viewportWidth - maxWidth - 10; | |||
} | |||
if (left < 10) { | |||
left = 10; | |||
} | |||
popup.style.left = left + 'px'; | |||
popup.style.top = top + 'px'; | |||
}; | |||
const hidePopup = () => { | |||
popup.style.display = 'none'; | |||
}; | |||
// >>>N リンクにホバーイベントを設定 | |||
document.querySelectorAll('.f-content a[href^="#post-"]').forEach(link => { | |||
const postId = link.getAttribute('href').replace('#post-', ''); | |||
link.addEventListener('mouseenter', () => { | |||
clearTimeout(hideTimer); | |||
showPopup(link, postId); | |||
}); | |||
link.addEventListener('mouseleave', () => { | |||
// 少し遅延させてチラつきを防ぐ | |||
hideTimer = setTimeout(hidePopup, 100); | |||
}); | |||
}); | |||
})(); | |||
// ========================================== | |||
} | } | ||
} // end startForum | } // end startForum | ||
2026年2月23日 (月) 17:09時点における最新版
// <nowiki>
(() => {
// 設定の読み込み待機
const checkConfig = () => {
if (!window.mw || !mw.config || !mw.loader) return;
// 設定がない場合のデフォルト値
mw.forum = mw.forum || {};
mw.forum.toppage = mw.forum.toppage || 'KenryoBBS';
mw.forum.threads = mw.forum.threads || 20;
mw.forum.sticky = mw.forum.sticky || [];
const fedit = mw.util.getParamValue('fedit');
// ページ判定(掲示板ページでなければ終了)
if (!new RegExp(`^${mw.forum.toppage}(|/.*)$`).test(mw.config.get('wgPageName')) || mw.config.get('wgAction') != 'view') {
return;
}
// 編集モード時のユーザーチェック
if (fedit && document.querySelector(`[id="${fedit}-user"]`) && document.querySelector(`[id="${fedit}-user"]`).innerText != mw.config.get('wgUserName')) {
return;
}
// UIモジュールの読み込み
mw.loader.load(['mediawiki.ui.button', 'mediawiki.ui.input', 'mediawiki.ui.checkbox']);
// ========== 直接編集の非表示(管理者以外) ==========
// 掲示板ページで「編集」「ソースを編集」タブを隠し、
// 直接ウィキテキストを操作して投稿を削除できないようにする。
// ※ JS無効化や直接APIアクセスは防げないため、完全な保護が
// 必要な場合は LocalSettings.php でページを保護してください。
if (!mw.config.get('wgUserGroups').includes('sysop')) {
const editBlockStyle = document.createElement('style');
editBlockStyle.textContent = [
'#ca-edit', // 「編集」タブ
'#ca-ve-edit', // VisualEditorタブ
'#ca-editsource', // 「ソースを編集」タブ
'.mw-editsection', // 節編集リンク
'#t-permalink', // 念のため固定リンクも非表示
'#ca-history', // 「履歴を表示」タブ
'#t-upload', // サイドバーのアップロードリンク
].join(',') + '{display:none!important;}';
document.head.appendChild(editBlockStyle);
}
// =====================================================
mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api'], function () {
// 動物リスト
const animals = [
'イヌ', 'ネコ', 'ハムスター', 'ウシ', 'ウマ', 'ヒツジ', 'ヤギ', 'ブタ', 'ニワトリ', 'アヒル',
'クマ', 'サル', 'ゴリラ', 'チンパンジー', 'オランウータン', 'マンドリル',
'チーター', 'ヒョウ', 'ジャガー', 'ピューマ', 'ハイエナ', 'ミーアキャット',
'シマウマ', 'ガゼル', 'インパラ', 'バッファロー', 'ヌー', 'アルパカ', 'リャマ', 'ラクダ',
'カンガルー', 'ワラビー', 'ウォンバット', 'カモノハシ', 'ハリモグラ',
'アザラシ', 'アシカ', 'セイウチ', 'オットセイ', 'ジュゴン', 'マナティ', 'ラッコ',
'シャチ', 'ベルーガ', 'マッコウクジラ', 'ザトウクジラ', 'ジンベエザメ', 'ホオジロザメ', 'ハンマーヘッドシャーク',
'エイ', 'マンボウ', 'タツノオトシゴ', 'ウツボ', 'クリオネ', 'クラゲ', 'サンゴ', 'イソギンチャク',
'ワシ', 'トンビ', 'ハト', 'インコ', 'オウム', 'ブンチョウ', 'カナリア',
'ダチョウ', 'エミュー', 'ヒクイドリ', 'キウィ', 'フラミンゴ', 'クジャク', 'ハクチョウ',
'カワセミ', 'ハヤブサ', 'ミミズク', 'キツツキ', 'カモメ', 'ペリカン',
'ワニ', 'ヘビ', 'トカゲ', 'カメレオン', 'イグアナ', 'ヤモリ', 'コモドドラゴン',
'カエル', 'イモリ', 'サンショウウオ', 'ウーパールーパー',
'アリクイ', 'アルマジロ', 'ヤマアラシ', 'スカンク', 'イタチ', 'テン', 'カワウソ', 'ビーバー',
'ムササビ', 'モモンガ', 'フェレット', 'チンチラ', 'デグー', 'モルモット',
'ドラゴン', 'ユニコーン', 'ペガサス', 'フェニックス', 'グリフォン', 'ケンタウロス', 'ケルベロス',
'ヒドラ', 'キマイラ', 'バジリスク', 'コカトリス', 'ワイバーン', 'マンティコア', 'ゴーレム',
'クラーケン', 'リヴァイアサン', 'シーサーペント', '人魚', 'セイレーン',
'カッパ', '天狗', '鬼', '龍', '麒麟', '鳳凰', '霊亀', '八咫烏',
'ツチノコ', 'ネッシー', 'イエティ', 'ビッグフット', 'チュパカブラ', 'モンゴリアンデスワーム',
'カブトムシ', 'クワガタ', 'チョウ', 'トンボ', 'セミ', 'カマキリ', 'ハチ', 'アリ',
'クモ', 'サソリ', 'ムカデ', 'ダンゴムシ', 'ペンギン', 'イルカ', 'コアラ', 'パンダ', 'レッサーパンダ',
'カピバラ', 'ナマケモノ', 'オポッサム', 'ミツバチ', 'テントウムシ', 'ホタル', 'カブトガニ', 'ダイオウグソクムシ',
'シーラカンス', 'オウムガイ', 'アノマロカリス', 'ウミサソリ', 'トリロバイト', 'ディメトロドン', 'エダフォサウルス',
'プレシオサウルス', 'モササウルス', 'ティラノサウルス', 'トリケラトプス', 'ステゴサウルス', 'ブラキオサウルス',
'アンキロサウルス', 'ディプロドクス', 'ヴェロキラプトル', 'ギガノトサウルス', 'カルノタウルス', 'メガロサウルス',
'スピノサウルス', 'イグアノドン', 'パラサウロロフス', 'エドモントサウルス', 'ヒプシロフォドン', 'マイアサウラ',
'プロトケラトプス', 'オヴィラプトル', 'オルニトミムス', 'シノサウルス', 'サウロロフス', 'サウルハドン', 'ガリミムス',
'アルバートサウルス', 'ダコタラプトル', 'アクロカントサウルス', 'シェノサウルス', 'エラスモサウルス', 'イクチオサウルス',
'オルニトレステス', 'テリジノサウルス', 'アマルガサウルス', 'シンラプトル'
];
const isAnon = mw.user.isAnon();
const isAdmin = mw.config.get('wgUserGroups').includes('sysop');
// ユーザー名(IPなど)をハッシュ化して動物インデックスに変換
const stringToHash = (str) => {
let hash = 0;
if (!str || str.length === 0) return hash;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
};
// ========== トリップ機能 ==========
// SHA-256を使ってトリップコードを生成する
// 入力: 秘密のキー文字列
// 出力: "◆" + Base64の先頭10文字
const computeTrip = async (key) => {
const encoder = new TextEncoder();
const data = encoder.encode(key);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const bytes = new Uint8Array(hashBuffer);
// バイト列をBase64文字列に変換
let binary = '';
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
const base64 = btoa(binary);
return '◆' + base64.slice(0, 10);
};
// localStorageから匿名ユーザーの名前設定を読み込む
const loadAnonSettings = () => {
try {
const saved = localStorage.getItem('forum_anon_settings');
return saved ? JSON.parse(saved) : null;
} catch (e) { return null; }
};
// 匿名ユーザーの名前設定をlocalStorageに保存する
const saveAnonSettings = (name, tripKey) => {
try {
localStorage.setItem('forum_anon_settings', JSON.stringify({ name, tripKey }));
} catch (e) { }
};
// ======================================
(async () => {
if (isAnon) {
// デフォルトの匿名名(IPハッシュベース)
const identifier = mw.user.getName() || mw.user.sessionId();
const index = stringToHash(identifier) % animals.length;
const defaultName = '匿名' + animals[index];
// 保存済み設定があればそれを使う
const saved = loadAnonSettings();
mw.forum.anonDefaultName = defaultName;
mw.forum.anonName = (saved && saved.name) ? saved.name : defaultName;
mw.forum.anonTripKey = (saved && saved.tripKey) ? saved.tripKey : '';
// トリップがあれば計算してusernameに反映
if (mw.forum.anonTripKey) {
const trip = await computeTrip(mw.forum.anonTripKey);
mw.forum.username = mw.forum.anonName + trip;
} else {
mw.forum.username = mw.forum.anonName;
}
} else {
mw.forum.username = mw.config.get('wgUserName');
}
if (!mw.forum.username) {
// フォールバック
try {
const token = await fetch(mw.config.get('wgArticlePath').replace('$1', "特別:トークページ")).then(r => r.text());
const doc = new DOMParser().parseFromString(token, 'text/html');
const title = doc.querySelector('.mw-page-title-main');
if (title) mw.forum.username = title.innerText;
} catch (e) { }
}
// メイン処理の開始
startForum();
})();
// メイン処理関数
function startForum() {
// ========== 名前・トリップ設定UIのHTML ==========
// 匿名ユーザーのみ投稿フォームの上部に表示される
const anonSettingsHtml = isAnon ? `<details id="f-name-settings" style="margin-bottom:.8em;border:1px solid #ccc;padding:.5em;border-radius:3px;">
<summary style="cursor:pointer;font-weight:bold;user-select:none;">名前・トリップの設定</summary>
<div style="margin-top:.5em;">
<p style="margin:.2em 0 .5em;font-size:.9em;color:#555;">
トリップキーを設定すると、投稿名の後ろに <b>◆記号+固有コード</b> が付き、なりすましを防げます。<br>
キーは他人に見えませんが、同じキーからは常に同じコードが生成されます。
</p>
<label style="display:block;margin-bottom:.4em;">
名前:
<input type="text" id="f-anon-name" class="mw-ui-input" placeholder="例: 名無しさん" style="width:200px;display:inline-block;margin-left:.5em;">
</label>
<label style="display:block;margin-bottom:.4em;">
トリップキー:
<input type="text" id="f-anon-trip" class="mw-ui-input" placeholder="秘密のキー(省略可・日本語可)" style="width:200px;display:inline-block;margin-left:.5em;" autocomplete="off">
</label>
<span id="f-trip-preview" style="font-size:.9em;color:#555;display:block;margin-bottom:.4em;"></span>
<input type="button" id="f-save-name" value="設定を保存" class="mw-ui-button">
</div>
</details>` : '';
// =================================================
// ========== 書式ツールバーHTML ==========
const toolbarHtml = `<div id="f-toolbar" style="margin-bottom:.4em;display:flex;gap:.3em;flex-wrap:wrap;align-items:center;padding:.3em;background:#f8f8f8;border:1px solid #ccc;border-radius:3px;">
<span style="font-size:.85em;color:#555;margin-left:.3em;">文字色:</span>
${['#cc0000', '#e07000', '#007700', '#0055cc', '#7700aa', '#555555'].map(c =>
`<span class="f-tb-color" data-color="${c}" title="${c}" style="display:inline-block;width:1.2em;height:1.2em;background:${c};border:2px solid #aaa;border-radius:2px;cursor:pointer;vertical-align:middle;"></span>`
).join('')}
</div>`;
// =========================================
const msg = Object.assign({
loading: '<p>読み込み中...</p>',
postform: `<div id="f-form"><h2>投稿${fedit ? `の編集 (<a href="#post-${fedit}")>#${fedit}</a>)` : ''}</h2>${anonSettingsHtml}<div class="mw-ui-checkbox" style="margin-bottom:.3em;${fedit ? 'display:none;' : ''}"><input type="checkbox" class="mw-ui-checkbox" id="f-reply-cb"><label for="f-reply-cb" style="user-select:none;">返信する</label></div><input type="number" id="f-reply" class="mw-ui-input" style="widht:50%;margin-bottom:.5em;">${toolbarHtml}<textarea accesskey="," id="wpTextbox1" cols="80" rows="25" class="mw-editfont-monospace"></textarea><input type="button" id="f-post" value="投稿" class="mw-ui-button mw-ui-progressive" style="margin-top:.5em;"><input type="button" id="f-preview" value="プレビュー" class="mw-ui-button" style="margin: .5em 0 0 .5em;"><fieldset hidden><legend>プレビュー</legend><div id="f-preview-content"></div></fieldset><style>.mw-ui-checkbox:has(#f-reply-cb:checked)+input{display:block;}#f-reply{display:none;}</style></div>`,
postsummary: 'post',
replysummary: 'reply to',
editsummary: 'edit',
deletesummary: 'delete post',
deletethreadsummary: 'スレッドの削除',
toppage_css: "<style>.f-sticky>td:first-child>a::before{content:'';background-image:url(https://upload.wikimedia.org/wikipedia/commons/a/a5/OOjs_UI_icon_pushPin.svg);width:.8em;height:.8em;margin-right:.2em;display:inline-block;background-size:.8em}#f-loadmore{display:block;margin-left:auto;margin-right:auto;}#f-create-btn-mobile{display:none;margin:1em 0;text-align:right;}@media (max-width:768px){.mw-indicators{display:block!important;}#f-create-btn-mobile{display:block;}}#f-notice{background:#fffacd;border:1px solid #daa520;border-radius:4px;padding:1em;margin:1em 0;font-size:.95em;line-height:1.6;}</style>",
toppage_notice: mw.forum.notice ? `<div id="f-notice">${mw.forum.notice}</div>` : '',
load_more: '<input type="button" value="もっと読み込む" class="mw-ui-button" id="f-loadmore">',
create: '<input type="button" class="mw-ui-button mw-ui-progressive" value="スレッドを作成">',
createform: `<div id="f-form"><label>スレッド名: <input type="text" id="f-threadname" class="mw-ui-input" style="margin-bottom:.5em;"></label>${toolbarHtml}<textarea accesskey="," id="wpTextbox1" cols="80" rows="25" class="mw-editfont-monospace"></textarea><input type="button" value="スレッドを作成" id="f-create" class="mw-ui-button mw-ui-progressive" style="margin-top:.5em;"><input type="button" id="f-preview" value="プレビュー" class="mw-ui-button" style="margin: .5em 0 0 .5em;"><fieldset hidden><legend>プレビュー</legend><div id="f-preview-content"></div></fieldset></div>`,
createthreadsummary: 'スレッドの作成',
posterror: 'エラー: 投稿できませんでした',
deleteerror: 'エラー: 削除できませんでした',
gotoform: '<a href="#wpTextbox1"><input type="button" value="投稿フォームへ" class="mw-ui-button" id="f-loadmore"></a>',
editlink: `<a href="${(() => { const url = new URL(location.href); url.searchParams.set('fedit', '$1'); return url; })()}" style="margin-left:7px;">編集</a>`
}, mw.forum.msg);
const func = {
getthreads: async (cont) => {
return await new mw.Api().get({
action: "query",
format: "json",
list: "allpages",
formatversion: "2",
[(cont ? 'apcontinue' : 'apprefix')]: (cont ?? new mw.Title(mw.forum.toppage).title + '/'),
apnamespace: new mw.Title(mw.forum.toppage).namespace,
apfilterredir: "nonredirects",
aplimit: mw.forum.threads
}).then(async (allpages) => {
return await new mw.Api().get({
action: "parse",
format: "json",
text: `{{#invoke:Thread|main|1=${cont ? '{}' : JSON.stringify(mw.forum.sticky)}|2=${JSON.stringify(allpages.query.allpages)}${cont ? '|3=true' : ''}}}`,
prop: "text",
wrapoutputclass: "",
contentmodel: "wikitext",
formatversion: "2"
}).then((parse) => {
return [parse.parse.text, (allpages.continue ? allpages.continue.apcontinue : undefined)];
});
});
},
preview: function () {
new mw.Api().get({
action: "parse",
format: "json",
text: document.querySelector("#wpTextbox1").value,
prop: "text",
preview: "true",
wrapoutputclass: "",
contentmodel: "wikitext",
disablelimitreport: "1",
formatversion: "2"
}).then((parse) => {
document.querySelector("#f-form > fieldset").hidden = false;
document.querySelector("#f-preview-content").innerHTML = `<div style="white-space:pre;">${parse.parse.text}</div>`;
});
},
deletePost: async (postId) => {
if (!confirm('本当にこの投稿を削除しますか?')) return;
const source = await (await fetch(mw.config.get('wgScript') + `?title=${mw.config.get('wgPageName')}&action=raw`)).text();
const newText = source.replace(new RegExp(`(\\{\\{post\\|(.*?)\\|${postId}\\|(.*?)\\|4=)((.|\n)*?)(}})`), '$1[Deleted]$6');
fetch('/wiki/forum-proxy.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: mw.config.get('wgPageName'),
text: newText,
summary: `${msg.deletesummary} #${postId}`
})
}).then(r => r.json()).then(data => {
if (data.result === 'Success') {
location.reload();
} else {
mw.notify(msg.deleteerror);
}
}).catch(() => {
mw.notify(msg.deleteerror);
});
},
// ========== スレッド削除(管理者のみ) ==========
deleteThread: async () => {
const threadName = mw.config.get('wgPageName').replace(mw.forum.toppage + '/', '');
if (!confirm(`スレッド「${threadName}」を削除しますか?\nこの操作は取り消せません。`)) return;
fetch('/wiki/forum-proxy.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'delete',
title: mw.config.get('wgPageName'),
reason: msg.deletethreadsummary
})
}).then(r => r.json()).then(data => {
if (data.result === 'Success') {
location.href = mw.config.get('wgArticlePath').replace('$1', mw.forum.toppage);
} else {
mw.notify(msg.deleteerror);
}
}).catch(() => mw.notify(msg.deleteerror));
},
// =================================================
// ========== 書式ツールバーの初期化 ==========
initToolbar: () => {
const toolbar = document.querySelector('#f-toolbar');
if (!toolbar) return;
const ta = document.querySelector('#wpTextbox1');
// 選択範囲をマークアップで囲むヘルパー
const wrapText = (start, end) => {
const s = ta.selectionStart, e = ta.selectionEnd;
const selected = ta.value.substring(s, e);
ta.value = ta.value.substring(0, s) + start + selected + end + ta.value.substring(e);
ta.selectionStart = s + start.length;
ta.selectionEnd = s + start.length + selected.length;
ta.focus();
};
// 太字・斜体ボタン
toolbar.querySelectorAll('.f-tb-btn').forEach(btn => {
btn.onclick = () => wrapText(btn.dataset.start, btn.dataset.end);
});
// 文字色ボタン
toolbar.querySelectorAll('.f-tb-color').forEach(btn => {
btn.onclick = () => wrapText(`<span style="color:${btn.dataset.color}">`, '</span>');
});
},
// =============================================
// ========== 名前設定UIの初期化 ==========
initAnonSettings: () => {
if (!isAnon) return;
const nameInput = document.querySelector('#f-anon-name');
const tripInput = document.querySelector('#f-anon-trip');
const preview = document.querySelector('#f-trip-preview');
const saveBtn = document.querySelector('#f-save-name');
if (!nameInput || !tripInput) return;
// 保存済み値をフォームに反映
nameInput.value = mw.forum.anonName;
tripInput.value = mw.forum.anonTripKey;
// プレビュー更新関数
const updatePreview = async () => {
const name = nameInput.value.trim() || mw.forum.anonDefaultName;
const key = tripInput.value;
if (key) {
const trip = await computeTrip(key);
preview.textContent = `投稿名プレビュー: ${name}${trip}`;
} else {
preview.textContent = `投稿名プレビュー: ${name}`;
}
};
nameInput.addEventListener('input', updatePreview);
tripInput.addEventListener('input', updatePreview);
updatePreview(); // 初期表示
// 保存ボタン
saveBtn.onclick = async () => {
const newName = nameInput.value.trim() || mw.forum.anonDefaultName;
const newTripKey = tripInput.value;
saveAnonSettings(newName, newTripKey);
mw.forum.anonName = newName;
mw.forum.anonTripKey = newTripKey;
if (newTripKey) {
const trip = await computeTrip(newTripKey);
mw.forum.username = newName + trip;
} else {
mw.forum.username = newName;
}
mw.notify('名前設定を保存しました。次の投稿から反映されます。');
};
}
// =========================================
};
const mc = document.querySelector('#mw-content-text>.mw-parser-output');
if (!mc) return;
if (mw.config.get('wgPageName') == mw.forum.toppage) {
// トップページ処理
if (mw.util.getParamValue('newthread') == '1') {
mc.innerHTML = msg.createform;
document.querySelector("#f-preview").onclick = func.preview;
func.initToolbar();
mw.loader.using('ext.wikiEditor');
document.querySelector('#f-create').onclick = function () {
// ホームへ戻るリンクをページ先頭に追加
let content = `[[${mw.forum.toppage}|← ${mw.forum.toppage}に戻る]]\n`;
if (mw.forum.zeroTemplate) {
content += `{{post|System|0|{{subst:#timel:Y/m/d H:i:s}}|4=${mw.forum.zeroTemplate}}}\n`;
}
// スレッド作成者がログイン済みかどうかで anon フラグを付与
const anonParam = isAnon ? '|anon=1' : '';
content += `{{post|${mw.forum.username}|1|{{subst:#timel:Y/m/d H:i:s}}|4=${document.querySelector('#wpTextbox1').value}${anonParam}}}`;
fetch('/wiki/forum-proxy.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: `${mw.forum.toppage}/${document.querySelector('#f-threadname').value}`,
text: content,
summary: msg.createthreadsummary,
protect: true
})
}).then(r => r.json()).then(data => {
if (data.result === 'Success') {
location.href = `${mw.config.get('wgArticlePath').replace('$1', mw.forum.toppage)}/${document.querySelector('#f-threadname').value}`;
} else {
mw.notify(msg.posterror);
}
}).catch(() => {
mw.notify(msg.posterror);
});
};
return;
}
mc.innerHTML = msg.loading;
func.getthreads().then((res) => {
mc.innerHTML = msg.toppage_notice + res[0] + msg.toppage_css;
// モバイル用の「スレッドを作成」ボタンを追加
const mobileBtn = document.createElement('div');
mobileBtn.id = 'f-create-btn-mobile';
mobileBtn.innerHTML = msg.create;
mc.insertBefore(mobileBtn, mc.firstChild);
mobileBtn.querySelector('input').onclick = function () {
const url = new URL(window.location.href);
url.searchParams.set('newthread', '1');
location.href = url;
};
if (res[1]) {
document.querySelector('#mw-content-text').innerHTML += msg.load_more;
document.querySelector('#f-loadmore').onclick = function () {
func.getthreads(res[1]).then((mres) => {
document.querySelector('#mw-content-text>.mw-parser-output>table>tbody').innerHTML += mres[0];
if (!mres[1]) {
document.querySelector('#f-loadmore').remove();
}
});
};
}
});
const indicators = document.querySelector('.mw-indicators');
if (indicators) {
indicators.innerHTML = msg.create;
indicators.querySelector("input").onclick = function () {
const url = new URL(window.location.href);
url.searchParams.set('newthread', '1');
location.href = url;
};
}
} else {
// スレッドページ処理
mc.innerHTML += msg.postform;
document.querySelector("#f-preview").onclick = func.preview;
mw.loader.using('ext.wikiEditor');
const indicators = document.querySelector('.mw-indicators');
if (indicators) indicators.innerHTML = msg.gotoform;
// 名前設定UIを初期化(匿名ユーザーのみ)
func.initAnonSettings();
// 書式ツールバーを初期化
func.initToolbar();
// 管理者にスレッド削除ボタンを表示
if (isAdmin) {
const delThreadBtn = document.createElement('input');
delThreadBtn.type = 'button';
delThreadBtn.value = 'スレッドを削除';
delThreadBtn.className = 'mw-ui-button mw-ui-destructive';
delThreadBtn.style.cssText = 'margin-left:.5em;';
delThreadBtn.onclick = func.deleteThread;
const ind = document.querySelector('.mw-indicators');
if (ind) ind.appendChild(delThreadBtn);
}
document.querySelector('#f-post').onclick = (async () => {
document.querySelector('#f-post').disabled = true;
const source = (await (await fetch(mw.config.get('wgScript') + `?title=${mw.config.get('wgPageName')}&action=raw`)).text());
const lp = source.split('{{post|').length - 1; // 0スレッドを考慮して投稿数+1をIDにする
let summary;
if (fedit) {
summary = `${msg.editsummary} [[${mw.config.get('wgPageName')}#post-${fedit}|#${fedit}]]`;
} else if (document.querySelector('#f-reply-cb').checked) {
const replyToUser = document.querySelector('[id="' + document.querySelector('#f-reply').value + '-user"]').innerText;
summary = `${msg.postsummary} [[${mw.config.get('wgPageName')}#post-${lp}|#${lp}]] ${msg.replysummary} [[User:${replyToUser}|${replyToUser}]]`;
} else {
summary = `${msg.postsummary} [[${mw.config.get('wgPageName')}#post-${lp}|#${lp}]]`;
}
// 匿名ユーザーには |anon=1 を付与してテンプレートでリンクを非表示にする
const anonParam = isAnon ? '|anon=1' : '';
// プロキシ経由でBot名義に編集する
const postBody = {
title: mw.config.get('wgPageName'),
summary: summary
};
if (fedit) {
postBody.text = source.replace(new RegExp(`\\{\\{post\\|(.*?)\\|${fedit}\\|(.*?)\\|4=((.|\n)*?)}}`), `{{post|$1|${fedit}|$2|4=${document.querySelector('#wpTextbox1').value}}}`);
} else {
postBody.appendtext = `\n{{post|${mw.forum.username}|${lp}|{{subst:#timel:Y/m/d H:i:s}}|4=${document.querySelector('#wpTextbox1').value}${document.querySelector('#f-reply-cb').checked ? '|re=' + document.querySelector('#f-reply').value : ''}${anonParam}}}`;
}
fetch('/wiki/forum-proxy.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postBody)
}).then(r => r.json()).then(data => {
if (data.result === 'Success') {
if (fedit) {
location.href = `${mw.util.getUrl()}#post-${fedit}`;
} else {
location.reload();
}
} else {
mw.notify(msg.posterror);
document.querySelector('#f-post').disabled = false;
}
}).catch(() => {
mw.notify(msg.posterror);
document.querySelector('#f-post').disabled = false;
});
});
if (fedit) {
location.hash = '#f-form';
document.querySelector('#wpTextbox1').focus();
(async () => { (await (await fetch(mw.config.get('wgScript') + `?title=${mw.config.get('wgPageName')}&action=raw`)).text()).replace(new RegExp(`\\{\\{post\\|(.*?)\\|${fedit}\\|(.*?)\\|4=((.|\n)*?)}}`), (m, u, d, t) => { document.querySelector('#wpTextbox1').value = t; }) })();
}
if (!isAnon) {
document.querySelectorAll('.f-u-' + mw.config.get('wgUserName')).forEach(el => {
const numText = el.querySelector('.f-number-text').innerText;
const postId = numText.replace('#', '');
el.innerHTML += msg.editlink.replace('%241', postId);
});
}
if (isAdmin) {
document.querySelectorAll('.f-header').forEach(header => {
const delSpan = document.createElement('span');
delSpan.style.marginLeft = '10px';
const delLink = document.createElement('a');
delLink.textContent = '削除';
delLink.style.color = '#d33';
delLink.style.cursor = 'pointer';
delLink.onclick = function () {
const numText = header.querySelector('.f-number-text').innerText;
const postId = numText.replace('#', '');
func.deletePost(postId);
};
delSpan.appendChild(delLink);
header.appendChild(delSpan);
});
}
// ========== レスホバープレビュー ==========
// >>>N のリンクにマウスを乗せると該当投稿をポップアップ表示する
(() => {
// ポップアップ用の浮動div(ページに1つだけ作成)
const popup = document.createElement('div');
popup.id = 'f-hover-popup';
popup.style.cssText = [
'position:fixed',
'z-index:9999',
'max-width:480px',
'min-width:200px',
'background:#fff',
'border:2px solid #BBBBBB',
'border-radius:3px',
'box-shadow:2px 4px 12px rgba(0,0,0,.2)',
'pointer-events:none', // ポップアップ自体はマウスイベントを無視
'display:none',
'font-size:.9em',
'line-height:1.4',
].join(';');
document.body.appendChild(popup);
let hideTimer = null;
const showPopup = (anchorEl, postId) => {
const target = document.querySelector(`#post-${postId}`);
if (!target) return;
// 対象投稿をクローンしてポップアップに表示
const clone = target.cloneNode(true);
clone.querySelectorAll('a[href*="fedit"], span[style*="#d33"]').forEach(el => el.remove());
popup.innerHTML = '';
popup.appendChild(clone);
// ポップアップの位置をリンクの近くに配置(モバイル対応)
const rect = anchorEl.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// ポップアップの最大幅を画面幅の90%に制限
const maxWidth = Math.min(480, viewportWidth * 0.9);
popup.style.maxWidth = maxWidth + 'px';
// 一度表示して実際の高さを取得
popup.style.display = 'block';
popup.style.visibility = 'hidden';
const popupHeight = popup.offsetHeight;
popup.style.visibility = 'visible';
// 縦位置:下に余裕があれば下、なければ上
const spaceBelow = viewportHeight - rect.bottom;
const top = spaceBelow > popupHeight + 10
? rect.bottom + 4
: Math.max(4, rect.top - popupHeight - 4);
// 横位置:画面からはみ出ないように調整
let left = rect.left;
if (left + maxWidth > viewportWidth - 10) {
left = viewportWidth - maxWidth - 10;
}
if (left < 10) {
left = 10;
}
popup.style.left = left + 'px';
popup.style.top = top + 'px';
};
const hidePopup = () => {
popup.style.display = 'none';
};
// >>>N リンクにホバーイベントを設定
document.querySelectorAll('.f-content a[href^="#post-"]').forEach(link => {
const postId = link.getAttribute('href').replace('#post-', '');
link.addEventListener('mouseenter', () => {
clearTimeout(hideTimer);
showPopup(link, postId);
});
link.addEventListener('mouseleave', () => {
// 少し遅延させてチラつきを防ぐ
hideTimer = setTimeout(hidePopup, 100);
});
});
})();
// ==========================================
}
} // end startForum
});
};
if (window.mw && window.mw.loader) {
checkConfig();
} else {
window.addEventListener('load', checkConfig);
}
})();
// </nowiki>