// ==UserScript== // @name Futaba Reply Form Free Drag // @namespace http://tampermonkey.net/ // @version 1.1 // @description 二次元裏@aimgの返信フォームを自由に移動・最小化可能にする // @author You // @match https://nijiurachan.net/pc/* // @grant none // ==/UserScript== (function() { 'use strict'; const STORAGE_KEY_X = 'reply_form_left'; const STORAGE_KEY_Y = 'reply_form_top'; const STORAGE_KEY_MINIMIZED = 'reply_form_minimized'; let isDragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; function initFreeDrag() { const container = document.getElementById('reply-form-container'); if (!container) return; // 元のtoggleReplyForm関数を上書き window.toggleReplyForm = function() { toggleMinimize(container); }; // positionをfixedに変更(既にfixedだが念のため) container.style.position = 'fixed'; // 保存された位置を復元 const savedLeft = localStorage.getItem(STORAGE_KEY_X); const savedTop = localStorage.getItem(STORAGE_KEY_Y); const savedMinimized = localStorage.getItem(STORAGE_KEY_MINIMIZED); if (savedLeft !== null) { container.style.left = 'auto'; container.style.right = 'auto'; container.style.left = savedLeft + 'px'; } if (savedTop !== null) { container.style.top = savedTop + 'px'; } // 最小化状態を復元 if (savedMinimized === 'true') { minimizeForm(container, true); } // ドラッグハンドルを取得 const handles = container.querySelectorAll('.drag-handle'); // タブもドラッグ可能にする const tab = container.querySelector('.reply-form-tab'); const allDraggables = [...(handles || [])]; if (tab) { allDraggables.push(tab); } // 各ドラッグ可能要素にイベントを設定 allDraggables.forEach(function(element) { // マウス操作 element.addEventListener('mousedown', function(e) { // タブの場合は、クリック判定のため少し待つ if (element === tab) { const clickX = e.clientX; const clickY = e.clientY; let moved = false; const moveHandler = function(moveE) { const deltaX = Math.abs(moveE.clientX - clickX); const deltaY = Math.abs(moveE.clientY - clickY); // 5px以上動いたらドラッグと判定 if (deltaX > 5 || deltaY > 5) { moved = true; if (!isDragging) { startDrag(clickX, clickY, container); } } }; const upHandler = function() { document.removeEventListener('mousemove', moveHandler); document.removeEventListener('mouseup', upHandler); // 動いていなければクリックとして処理(toggleが実行される) if (moved) { e.preventDefault(); e.stopPropagation(); } }; document.addEventListener('mousemove', moveHandler); document.addEventListener('mouseup', upHandler); } else { // ドラッグハンドルは即座にドラッグ開始 if (e.button !== 0) return; e.preventDefault(); startDrag(e.clientX, e.clientY, container); } }); // タッチ操作 element.addEventListener('touchstart', function(e) { if (e.touches.length === 1) { const touch = e.touches[0]; if (element === tab) { const clickX = touch.clientX; const clickY = touch.clientY; let moved = false; const moveHandler = function(moveE) { const moveTouch = moveE.touches[0]; const deltaX = Math.abs(moveTouch.clientX - clickX); const deltaY = Math.abs(moveTouch.clientY - clickY); if (deltaX > 5 || deltaY > 5) { moved = true; if (!isDragging) { e.preventDefault(); startDrag(clickX, clickY, container); } } }; const endHandler = function() { document.removeEventListener('touchmove', moveHandler); document.removeEventListener('touchend', endHandler); if (moved) { e.preventDefault(); e.stopPropagation(); } }; document.addEventListener('touchmove', moveHandler, { passive: false }); document.addEventListener('touchend', endHandler); } else { e.preventDefault(); startDrag(touch.clientX, touch.clientY, container); } } }, { passive: false }); }); // グローバルムーブイベント document.addEventListener('mousemove', function(e) { if (isDragging) { e.preventDefault(); moveForm(e.clientX, e.clientY, container); } }); document.addEventListener('mouseup', endDrag); document.addEventListener('touchmove', function(e) { if (isDragging) { e.preventDefault(); moveForm(e.touches[0].clientX, e.touches[0].clientY, container); } }, { passive: false }); document.addEventListener('touchend', endDrag); document.addEventListener('touchcancel', endDrag); } function startDrag(clientX, clientY, container) { startX = clientX; startY = clientY; // 現在の位置を取得 const rect = container.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; isDragging = true; container.style.transition = 'none'; document.body.style.userSelect = 'none'; document.body.style.webkitUserSelect = 'none'; document.body.style.cursor = 'grabbing'; } function moveForm(clientX, clientY, container) { const deltaX = clientX - startX; const deltaY = clientY - startY; let newLeft = startLeft + deltaX; let newTop = startTop + deltaY; // 画面内に収める const containerWidth = container.offsetWidth; const containerHeight = container.offsetHeight; const maxLeft = window.innerWidth - containerWidth; const maxTop = window.innerHeight - containerHeight; newLeft = Math.max(0, Math.min(maxLeft, newLeft)); newTop = Math.max(0, Math.min(maxTop, newTop)); // rightプロパティを削除してleftで制御 container.style.right = 'auto'; container.style.left = newLeft + 'px'; container.style.top = newTop + 'px'; } function endDrag() { if (isDragging) { const container = document.getElementById('reply-form-container'); isDragging = false; container.style.transition = ''; document.body.style.userSelect = ''; document.body.style.webkitUserSelect = ''; document.body.style.cursor = ''; // 位置を保存 const rect = container.getBoundingClientRect(); localStorage.setItem(STORAGE_KEY_X, Math.round(rect.left)); localStorage.setItem(STORAGE_KEY_Y, Math.round(rect.top)); } } function toggleMinimize(container) { const content = container.querySelector('.reply-form-content'); const isMinimized = content.style.display === 'none'; if (isMinimized) { // 最大化 minimizeForm(container, false); } else { // 最小化 minimizeForm(container, true); } } function minimizeForm(container, minimize) { const content = container.querySelector('.reply-form-content'); const tab = container.querySelector('.reply-form-tab'); const arrow = tab ? tab.querySelector('.tab-arrow') : null; if (minimize) { // 最小化 content.style.display = 'none'; if (arrow) arrow.textContent = '▶'; if (tab) tab.style.cursor = 'move'; // カーソルを変更 localStorage.setItem(STORAGE_KEY_MINIMIZED, 'true'); // collapsedクラスも削除して横スライドを無効化 container.classList.remove('collapsed'); } else { // 最大化 content.style.display = ''; if (arrow) arrow.textContent = '◀'; if (tab) tab.style.cursor = 'pointer'; // カーソルを戻す localStorage.setItem(STORAGE_KEY_MINIMIZED, 'false'); // collapsedクラスも削除 container.classList.remove('collapsed'); } } // ページ読み込み後に初期化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initFreeDrag); } else { initFreeDrag(); } // 念のため少し遅延させても実行 setTimeout(initFreeDrag, 500); })();