shibainurou
https://gyazo.com/03161171fcb088587262450d75970891
プロフィール
職業:なんでも屋エンジニア
ユーザーの御用聞から社内インフラ管理まで
年齢:おっさん
趣味:軽登山
2025/8/17
夏季休暇最終日
ずっと見る専門だったけど参加してみます。よろしくお願いします。
歓迎.iconいらっしゃい!suto3.iconmoeki.iconHiro Aki.iconsta.iconW7kA9pX2.iconbsahd.iconcak.iconkoyoshi.iconHyou.iconMijikko.iconmeganii.iconnomadoor.icon基素.iconmtane0412.iconwogikaze.icon
ありがとうございます!shibainurou.icon
code:script.js
// Cosense Link Preview UserScript
// マウスホバーでリンク先ページをプレビュー表示
(function() {
'use strict';
let previewContainer = null;
let currentHoveredLink = null;
let hoverTimer = null;
let isDebugMode = false; // デバッグモード(コンソールでcosenseLinkPreview.debug = trueで有効化)
// プレビューコンテナの作成
function createPreviewContainer() {
if (previewContainer) return previewContainer;
previewContainer = document.createElement('div');
previewContainer.id = 'cosense-link-preview';
previewContainer.className = 'link-preview-container';
previewContainer.style.cssText = `
position: fixed;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 12px;
width: 600px;
height: 70vh;
overflow: hidden;
z-index: 10000;
display: none;
font-size: 14px;
line-height: 1.4;
`;
// Webkitブラウザ用のスクロールバースタイル
const style = document.createElement('style');
style.textContent = `
width: 6px;
}
border-radius: 3px;
}
border-radius: 3px;
}
}
`;
if (!document.getElementById('cosense-preview-scrollbar-style')) {
style.id = 'cosense-preview-scrollbar-style';
document.head.appendChild(style);
}
document.body.appendChild(previewContainer);
return previewContainer;
}
// ページ内容を取得する関数
async function fetchPageContent(pageName) {
try {
// CosenseのAPIを使用してページ内容を取得
const projectName = location.pathname.split('/')1; const response = await fetch(/api/pages/${projectName}/${encodeURIComponent(pageName)});
if (!response.ok) {
throw new Error('Page not found');
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch page content:', error);
return null;
}
}
// プレビュー内容を生成(iframe版)
function generatePreviewContent(pageData, pageName) {
if (!pageData) {
}
const projectName = location.pathname.split('/')1; const pageUrl = /${projectName}/${encodeURIComponent(pageName)};
// iframeでページを表示
const iframeContent = `
<div style="margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #666; border-bottom: 1px solid #eee; padding-bottom: 6px;"> <span>${escapeHtml(pageData.title)}</span>
<a href="${pageUrl}" target="_blank" style="color: #0066cc; text-decoration: none;">新しいタブで開く ↗</a> </div>
<iframe
src="${pageUrl}"
style="
width: 100%;
height: calc(70vh - 40px);
border: none;
border-radius: 4px;
background: white;
"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-top-navigation"
loading="lazy">
</iframe>
`;
return iframeContent;
}
// HTMLエスケープ
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// プレビューを表示
function showPreview(link, pageData, pageName) {
const container = createPreviewContainer();
container.innerHTML = generatePreviewContent(pageData, pageName);
container.style.display = 'block';
// リンクの位置を基準にプレビューを配置
const linkRect = link.getBoundingClientRect();
let left = linkRect.left;
let top = linkRect.bottom + 5;
// 画面からはみ出る場合の調整
const previewWidth = 600;
const previewHeight = window.innerHeight * 0.7;
if (left + previewWidth > window.innerWidth) {
left = window.innerWidth - previewWidth - 20;
}
if (top + previewHeight > window.innerHeight) {
top = linkRect.top - previewHeight - 5;
}
container.style.left = ${Math.max(10, left)}px;
container.style.top = ${Math.max(10, top)}px;
}
// プレビューを隠す
function hidePreview() {
if (previewContainer) {
previewContainer.style.display = 'none';
}
currentHoveredLink = null;
if (hoverTimer) {
clearTimeout(hoverTimer);
hoverTimer = null;
}
}
// デバッグログ関数
function debugLog(message, data = null) {
if (isDebugMode) {
console.log([Cosense Link Preview] ${message}, data || '');
}
}
// リンクのマウスイベントハンドラー
function handleLinkHover(event) {
// プレビューコンテナ内のリンクは無視
if (event.target.closest('#cosense-link-preview')) {
debugLog('Link inside preview container, skipping');
return;
}
// より確実にリンクを検出
const link = event.target.closest('a') ||
(event.target.tagName === 'A' ? event.target : null);
if (!link) {
debugLog('No link found');
return;
}
// page-linkクラスを持つリンクのみ対象
if (!link.classList.contains('page-link')) {
debugLog('Link does not have page-link class, skipping', link.className);
return;
}
const href = link.getAttribute('href');
if (!href) {
debugLog('Link has no href attribute');
return;
}
// 内部リンクのみ対象(相対パスまたは同ドメイン)
const isInternalLink = href.startsWith('/') ||
(href.startsWith('http') && href.includes(location.host));
if (!isInternalLink) {
debugLog('External link, skipping', href);
return;
}
// フラグメント(#)のみのリンクは除外
if (href.startsWith('#')) {
debugLog('Fragment link, skipping', href);
return;
}
debugLog('Link hover detected', { href, text: link.textContent });
// 既存のタイマーをクリア
if (hoverTimer) {
clearTimeout(hoverTimer);
}
currentHoveredLink = link;
// ページ名を抽出
let pageName;
try {
if (href.startsWith('/')) {
// 相対パスの場合
const pathParts = href.split('/').filter(part => part);
} else {
// 絶対URLの場合
const url = new URL(href);
const pathParts = url.pathname.split('/').filter(part => part);
}
if (!pageName) {
debugLog('Could not extract page name from href', href);
return;
}
pageName = decodeURIComponent(pageName);
debugLog('Page name extracted', pageName);
// バックグラウンドでコンテンツの読み込みを開始
let pageDataPromise = fetchPageContent(pageName);
// 0.5秒後にプレビューウィンドウを表示
hoverTimer = setTimeout(async () => {
if (currentHoveredLink !== link) {
debugLog('Link changed during display timeout, aborting');
return;
}
try {
// 読み込み完了を待つ
const pageData = await pageDataPromise;
if (currentHoveredLink === link) {
debugLog('Showing preview', pageData ? 'with data' : 'without data');
showPreview(link, pageData, pageName);
} else {
debugLog('Link changed during fetch, not showing preview');
}
} catch (error) {
debugLog('Error displaying preview', error);
}
}, 500); // 500ms後にプレビューを表示
} catch (error) {
debugLog('Error in hover handler', error);
}
}
function handleLinkLeave(event) {
// プレビューコンテナ内のリンクは無視
if (event.target.closest('#cosense-link-preview')) {
return;
}
const link = event.target.closest('a') ||
(event.target.tagName === 'A' ? event.target : null);
if (!link) return;
// page-linkクラスを持つリンクのみ対象
if (!link.classList.contains('page-link')) {
return;
}
debugLog('Link leave detected', link.textContent);
// プレビューエリアに移動していない場合のみ隠す
setTimeout(() => {
if (!previewContainer || !previewContainer.matches(':hover')) {
debugLog('Hiding preview on link leave');
hidePreview();
} else {
debugLog('Not hiding preview - mouse over preview container');
}
}, 100);
}
// イベントリスナーを設定
function setupEventListeners() {
document.addEventListener('mouseover', handleLinkHover);
document.addEventListener('mouseout', handleLinkLeave);
// プレビューコンテナ自体のマウスイベント
document.addEventListener('mouseover', (event) => {
if (event.target.closest('#cosense-link-preview')) {
// プレビュー上にマウスがある場合は隠さない
if (hoverTimer) {
clearTimeout(hoverTimer);
hoverTimer = null;
}
}
});
document.addEventListener('mouseout', (event) => {
if (event.target.closest('#cosense-link-preview') &&
!event.relatedTarget?.closest('#cosense-link-preview')) {
hidePreview();
}
});
// ESCキーでプレビューを隠す
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
hidePreview();
}
});
}
// 初期化
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupEventListeners);
} else {
setupEventListeners();
}
// デバッグ機能をグローバルに公開
window.cosenseLinkPreview = {
debug: false,
setDebug: function(enabled) {
isDebugMode = enabled;
this.debug = enabled;
debugLog('Debug mode ' + (enabled ? 'enabled' : 'disabled'));
},
hidePreview: hidePreview,
showStats: function() {
console.log('Current state:', {
previewContainer: !!previewContainer,
currentHoveredLink: !!currentHoveredLink,
hoverTimer: !!hoverTimer
});
}
};
}
init();
})();