SIMPLE TEXT NOTE FOR COPY 浏览器浮窗记事本

创建一个可编辑的浮窗记事本,支持拖拽和位置记忆

// ==UserScript==
// @name         SIMPLE TEXT NOTE FOR COPY 浏览器浮窗记事本
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  创建一个可编辑的浮窗记事本,支持拖拽和位置记忆
// @author       leifeng
// @match        *://*/*
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 样式定义
    const style = `
        .floating-notepad {
            position: fixed;
            bottom: -310px;
            right: 20px;
            width: 300px;
            background-color: #e9f7e8;
            border-radius: 8px 8px 0 0;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            z-index: 9999;
            transition: all 0.3s ease;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
        }

        .floating-notepad.collapsed {
            bottom: -330px;
        }

        .notepad-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 5px 15px;
            background-color: #4a6cf7;
            color: white;
            border-radius: 8px 8px 0 0;
            user-select: none;
            cursor: move;
            height: 30px;
        }

        .notepad-header.over-limit {
            background-color: #dc3545;
        }

        .notepad-title {
            font-weight: 600;
            font-size: 15px;
        }

        .notepad-control-buttons {
            display: flex;
            align-items: center;
        }

        .notepad-toggle-btn, .notepad-center-btn, .notepad-hide-btn {
            font-size: 18px;
            cursor: pointer;
            padding: 0 10px; /* 增加按钮之间的间距 */
            transition: transform 0.2s ease;
        }

        .notepad-toggle-btn:hover, .notepad-center-btn:hover, .notepad-hide-btn:hover {
            transform: scale(1.2);
        }

        .notepad-footer {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-top: 5px;
            padding: 8px 15px;
            border-top: 1px solid #e0e0e0;
        }

        .notepad-count {
            font-size: 12px;
            color: #6c757d;
        }

        .notepad-clear-all {
            font-size: 12px;
            color: #dc3545;
            cursor: pointer;
            text-decoration: underline;
            transition: color 0.2s ease;
        }

        .notepad-clear-all:hover {
            color: #c82333;
        }

        .notepad-content {
            padding: 15px;
            max-height: 350px;
            display: flex;
            flex-direction: column;
        }

        .notepad-list {
            flex: 1;
            margin-bottom: 10px;
            max-height: 250px;
            overflow-y: auto;
            padding-right: 5px;
        }

        .notepad-item {
            padding: 8px 10px;
            margin-bottom: 8px;
            background-color: #ffffff;
            border: 1px solid #ced4da;
            border-radius: 4px;
            font-size: 14px;
            line-height: 1.4;
            min-height: 40px;
            transition: all 0.2s ease;
        }

        .notepad-item:hover {
            background-color: #f1f3f5;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
        }

        .notepad-item-empty {
            border: 1px dashed #ced4da;
            background-color: transparent;
            display: flex;
            align-items: center;
            color: #6c757d;
            font-size: 13px;
        }

        .notepad-item-empty:hover {
            border-color: #4a6cf7;
            color: #4a6cf7;
        }

        .notepad-item-text {
            padding: 0;
            color: #333333;
        }

        .notepad-item-char-count {
            font-size: 11px;
            color: #6c757d;
            margin-top: 3px;
            text-align: right;
        }

        .notepad-item-edit-area {
            width: 100%;
            padding: 5px;
            border: 1px solid #ced4da;
            border-radius: 4px;
            font-size: 14px;
            resize: none;
            margin-bottom: 3px;
            box-sizing: border-box;
            min-height: 40px;
            background-color: white;
            color: #333333;
            transition: all 0.2s ease;
        }

        .notepad-item-edit-area.expanded {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: calc(100% - 40px);
            max-width: 500px;
            height: calc(100% - 100px);
            max-height: 400px;
            z-index: 10000;
            font-size: 16px;
            box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
        }
    `;

    // 添加样式
    GM_addStyle(style);

    // 存储键名
    const STORAGE_KEY = 'floating_notepad_items';
    const MAX_CHAR_COUNT = 500;
    const DEFAULT_TITLE = '浮窗记事本';

    // 创建记事本元素
    function createNotepad() {
        const container = document.createElement('div');
        container.className = 'floating-notepad';

        // 头部
        const header = document.createElement('div');
        header.className = 'notepad-header';

        const title = document.createElement('div');
        title.className = 'notepad-title';
        title.textContent = DEFAULT_TITLE;

        // 控制按钮容器
        const controlButtons = document.createElement('div');
        controlButtons.className = 'notepad-control-buttons';

        const centerBtn = document.createElement('div');
        centerBtn.className = 'notepad-center-btn';
        centerBtn.innerHTML = '⮙';
        centerBtn.addEventListener('click', centerNotepad);

        const toggleBtn = document.createElement('div');
        toggleBtn.className = 'notepad-toggle-btn';
        toggleBtn.innerHTML = '⮛';
        toggleBtn.addEventListener('click', toggleNotepad);

        const hideBtn = document.createElement('div');
        hideBtn.className = 'notepad-hide-btn';
        hideBtn.innerHTML = '✕';
        hideBtn.addEventListener('click', hideNotepad);

        controlButtons.appendChild(centerBtn); // 先添加 ⮙ 按钮
        controlButtons.appendChild(toggleBtn); // 再添加 ⮛ 按钮
        controlButtons.appendChild(hideBtn);   // 最后添加 ✕ 按钮

        header.appendChild(title);
        header.appendChild(controlButtons);

        // 内容区域
        const content = document.createElement('div');
        content.className = 'notepad-content';

        // 列表区域
        const listContainer = document.createElement('div');
        listContainer.className = 'notepad-list';

        // 底部区域
        const footer = document.createElement('div');
        footer.className = 'notepad-footer';

        const countIndicator = document.createElement('div');
        countIndicator.className = 'notepad-count';
        updateCountIndicator();

        const clearAllBtn = document.createElement('div');
        clearAllBtn.className = 'notepad-clear-all';
        clearAllBtn.textContent = '清除所有';
        clearAllBtn.addEventListener('click', clearAllNotes);

        footer.appendChild(countIndicator);
        footer.appendChild(clearAllBtn);

        content.appendChild(listContainer);
        content.appendChild(footer);

        container.appendChild(header);
        container.appendChild(content);
        document.body.appendChild(container);

        // 状态管理
        let isDragging = false;
        let initialDragPosition = { x: 0, y: 0 };
        let initialElementPosition = { bottom: 0, right: 20 };
        let expandedItemId = null;
        let currentEditingId = null;

        // 加载已保存的笔记
        loadNotes();

        // 窗口拖动功能
        header.addEventListener('mousedown', startDrag);

        // 点击外部关闭展开的编辑框
        document.addEventListener('click', (e) => {
            if (expandedItemId && !e.target.closest('.notepad-item-edit-area.expanded')) {
                collapseAllEditors();
            }
        });

        // ESC 键关闭展开的编辑框
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && expandedItemId) {
                collapseAllEditors();
            }
        });

        // 鼠标事件处理
        function startDrag(e) {
            if (e.target.closest('.notepad-control-buttons')) return;

            e.preventDefault();
            isDragging = true;

            const rect = container.getBoundingClientRect();
            initialDragPosition = { x: e.clientX, y: e.clientY };

            const computedStyle = getComputedStyle(container);
            initialElementPosition = {
                bottom: parseInt(computedStyle.bottom) || 0,
                right: parseInt(computedStyle.right) || 20
            };

            container.style.zIndex = "10000";

            // 拖拽时隐藏编辑框
            if (currentEditingId) {
                saveCurrentEdit();
                collapseAllEditors();
            }
        }

        function handleDrag(e) {
            if (!isDragging) return;

            e.preventDefault();

            const dx = e.clientX - initialDragPosition.x;
            const dy = e.clientY - initialDragPosition.y;

            container.classList.remove('top-position', 'collapsed');

            // 计算新位置
            let newBottom = initialElementPosition.bottom - dy;
            let newRight = initialElementPosition.right + dx;

            // 边界限制
            const minBottom = -container.offsetHeight + header.offsetHeight;
            const maxBottom = window.innerHeight - header.offsetHeight;
            const minRight = 0;
            const maxRight = window.innerWidth - container.offsetWidth;

            newBottom = Math.max(minBottom, Math.min(maxBottom, newBottom));
            newRight = Math.max(minRight, Math.min(maxRight, newRight));

            // 更新位置
            container.style.bottom = `${newBottom}px`;
            container.style.right = `${newRight}px`;
            container.style.top = 'auto';
        }

        function stopDrag() {
            if (!isDragging) return;

            isDragging = false;
            container.style.zIndex = "9999";
        }

        // 笔记管理功能
        function loadNotes() {
            let items = getSavedItems();

            if (items.length === 0) {
                items = Array.from({ length: 10 }, (_, i) => ({
                    id: Date.now() + i,
                    text: '',
                    timestamp: new Date().toISOString()
                }));
                saveItems(items);
            }

            renderNotes(items);
        }

        function renderNotes(items) {
            listContainer.innerHTML = '';

            items.forEach(item => {
                const noteEl = document.createElement('div');
                noteEl.className = item.text ? 'notepad-item' : 'notepad-item notepad-item-empty';
                noteEl.dataset.id = item.id;

                // 查看模式
                const viewMode = document.createElement('div');
                viewMode.className = 'notepad-item-view-mode';

                const textEl = document.createElement('div');
                textEl.className = 'notepad-item-text';
                textEl.textContent = item.text || '点击编辑...';

                const charCountEl = document.createElement('div');
                charCountEl.className = 'notepad-item-char-count';
                charCountEl.textContent = item.text ? `${item.text.length}/${MAX_CHAR_COUNT}` : `0/${MAX_CHAR_COUNT}`;

                viewMode.appendChild(textEl);
                viewMode.appendChild(charCountEl);

                // 编辑模式
                const editMode = document.createElement('div');
                editMode.className = 'notepad-item-edit-mode';
                editMode.style.display = 'none';

                const editArea = document.createElement('textarea');
                editArea.className = 'notepad-item-edit-area';
                editArea.value = item.text;
                editArea.maxLength = MAX_CHAR_COUNT;

                const editCharCountEl = document.createElement('div');
                editCharCountEl.className = 'notepad-item-char-count';
                editCharCountEl.textContent = `${item.text.length}/${MAX_CHAR_COUNT}`;

                editMode.appendChild(editArea);
                editMode.appendChild(editCharCountEl);

                // 添加到笔记元素
                noteEl.appendChild(viewMode);
                noteEl.appendChild(editMode);

                // 点击进入编辑模式
                noteEl.addEventListener('click', (e) => {
                    if (e.target.closest('.notepad-item-edit-area')) return;

                    if (container.classList.contains('collapsed')) {
                        toggleNotepad();
                    }

                    currentEditingId = item.id;
                    switchToEditMode(noteEl, item);

                    setTimeout(() => {
                        expandEditor(editArea, item.id);
                        checkCharCount(editArea);
                    }, 10);
                });

                // 编辑区域事件
                editArea.addEventListener('input', () => {
                    const length = editArea.value.length;
                    editCharCountEl.textContent = `${length}/${MAX_CHAR_COUNT}`;
                    checkCharCount(editArea);
                });

                editArea.addEventListener('paste', () => {
                    setTimeout(() => {
                        const length = editArea.value.length;
                        editCharCountEl.textContent = `${length}/${MAX_CHAR_COUNT}`;
                        checkCharCount(editArea);
                    }, 10);
                });

                editArea.addEventListener('blur', () => {
                    saveEditedNote(item.id, editArea.value);
                    collapseAllEditors();
                    resetHeader();
                    currentEditingId = null;
                });

                editArea.addEventListener('keydown', (e) => {
                    if (e.key === 'Enter') {
                        if (e.shiftKey) {
                            e.preventDefault();
                            editArea.blur();
                        }
                    }
                });

                listContainer.appendChild(noteEl);
            });

            updateCountIndicator(items.length);
        }

        function switchToEditMode(noteEl, item) {
            document.querySelectorAll('.notepad-item-edit-mode').forEach(el => {
                if (el.parentElement.dataset.id !== item.id) {
                    el.style.display = 'none';
                    el.parentElement.querySelector('.notepad-item-view-mode').style.display = 'block';
                    if (!el.parentElement.querySelector('.notepad-item-text').textContent.trim()) {
                        el.parentElement.classList.add('notepad-item-empty');
                    }
                }
            });

            const viewMode = noteEl.querySelector('.notepad-item-view-mode');
            const editMode = noteEl.querySelector('.notepad-item-edit-mode');

            noteEl.classList.remove('notepad-item-empty');
            viewMode.style.display = 'none';
            editMode.style.display = 'block';
            editMode.querySelector('.notepad-item-edit-area').focus();
        }

        function expandEditor(editArea, itemId) {
            collapseAllEditors();
            editArea.classList.add('expanded');
            expandedItemId = itemId;

            const cursorPos = editArea.selectionStart;
            setTimeout(() => {
                editArea.focus();
                editArea.setSelectionRange(cursorPos, cursorPos);
            }, 10);
        }

        function collapseAllEditors() {
            document.querySelectorAll('.notepad-item-edit-area.expanded').forEach(el => {
                el.classList.remove('expanded');
            });
            expandedItemId = null;
        }

        function saveEditedNote(id, newText) {
            newText = newText.trim();

            const items = getSavedItems();
            const itemIndex = items.findIndex(item => item.id === id);

            if (itemIndex !== -1) {
                items[itemIndex].text = newText.substring(0, MAX_CHAR_COUNT);
                items[itemIndex].timestamp = new Date().toISOString();
                saveItems(items);
                renderNotes(items);
            }

            resetHeader();
        }

        function saveCurrentEdit() {
            if (!currentEditingId) return;

            const editArea = document.querySelector(`.notepad-item[data-id="${currentEditingId}"] .notepad-item-edit-area`);
            if (editArea) {
                saveEditedNote(currentEditingId, editArea.value);
            }
        }

        function checkCharCount(editArea) {
            const length = editArea.value.length;

            if (length >= MAX_CHAR_COUNT) {
                title.textContent = '字符数达限制';
                header.classList.add('over-limit');
            } else {
                title.textContent = DEFAULT_TITLE;
                header.classList.remove('over-limit');
            }
        }

        function resetHeader() {
            title.textContent = DEFAULT_TITLE;
            header.classList.remove('over-limit');
        }

        function clearAllNotes() {
            if (confirm('确定要清除所有笔记吗?')) {
                const emptyItems = Array.from({ length: 10 }, (_, i) => ({
                    id: Date.now() + i,
                    text: '',
                    timestamp: new Date().toISOString()
                }));
                saveItems(emptyItems);
                renderNotes(emptyItems);
            }
        }

        function getSavedItems() {
            try {
                const saved = sessionStorage.getItem(STORAGE_KEY);
                return saved ? JSON.parse(saved) : [];
            } catch (e) {
                console.error('Failed to load notes:', e);
                return [];
            }
        }

        function saveItems(items) {
            try {
                sessionStorage.setItem(STORAGE_KEY, JSON.stringify(items));
            } catch (e) {
                console.error('Failed to save notes:', e);
            }
        }

        function updateCountIndicator(count = 0) {
            countIndicator.textContent = `共 ${count} 条笔记`;
        }

        function toggleNotepad() {
            container.classList.toggle('collapsed');

            if (container.classList.contains('collapsed')) {
                container.style.bottom = '-330px';
                container.style.top = 'auto';
            } else {
                container.style.bottom = '-310px';
                container.style.top = 'auto';
            }
        }

        function centerNotepad() {
            const windowHeight = window.innerHeight;
            const notepadHeight = container.offsetHeight;
            const centerPosition = (windowHeight - notepadHeight) / 2;

            container.classList.remove('top-position', 'collapsed');
            container.style.top = `${centerPosition}px`;
            container.style.bottom = 'auto';
        }

        function hideNotepad() {
            const windowHeight = window.innerHeight;
            const initialBottom = -310;

            container.classList.remove('top-position', 'collapsed');
            container.style.bottom = `${initialBottom - windowHeight}px`;
            container.style.top = 'auto';
        }

        // 添加鼠标事件监听
        document.addEventListener('mousemove', handleDrag);
        document.addEventListener('mouseup', stopDrag);
        document.addEventListener('mouseleave', stopDrag);
    }

    // 初始化记事本
    createNotepad();
})();