Google Contacts - Adjustable Column Width with Tooltips

Make Google Contacts table columns resizable by dragging and show full content on hover

当前为 2025-12-27 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Google Contacts - Adjustable Column Width with Tooltips
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Make Google Contacts table columns resizable by dragging and show full content on hover
// @author       aspen138
// @match        https://contacts.google.com/*
// @grant        none
// @license      MIT
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAEGklEQVR4Ae2bA5AcXRSF+7dt22bpN8JCPIht21yjsqXYmulCbBbCmY2dFGLbejfvpmaNmVeZzO3ZnK/qrLdf1Tndj7ctxwIAAAAAAAAANe2HLHfgL8sVHKq1TGuX1vmQdoV+NpT/5s7fRglQe82z2tQEbe4pLRWhTvH/3PnfuwC4Am0sd/BEjrHG4v/laxgCaq56Qhs3jU2MivhafE0QAQ02PK+7jwAbF1XxNfnaYcBA6wosNjLWQHxtDNClUSuYZW6qobiNYgDewK/aoFv3PABug9sqBHAHVrBBMRG3lQ/gXvcvGxNTcZshgCswPOYBcJtAQ/SAHhgPxzwAbpPbxlZD8Bs2RETcNvr/YGWxALht9P/BJmIBcNtY/QZ7iAXAbd/X4AnAGIAAMAvCOgBgJYy9IIDdUJwHAJyI4UwYoCoCdUEAlXGoDQVlujoaAAAAAAAAMGQavZPgo3qJfspKsmleoo/2dhxz5VjFwUfV5x32qLeab1fPNdgc8Qr4yTqb1YuNtqs3m+9R33U5qioPuapc6VSiaqXTflcardOfZ7pTqYsrlT6yyjrJNr2b7Kc+ST7aoY1XrCE+paqmnFCvNtka9Z3QZxtsVb90P6FqpSk2PbzSaJs7jRoPGEAPli3jffRdok22NvymlspRvayz0TM+TBC/9znLJpsEEf/lK9r0V5L8ND7H8Hx3vTbkQMxLUj5vf4CfBpMgUvlpiNc+3qXNPl3Y/P6Tb6pP2+1hQ0T0RrM9qnrKTZMQ5tYcQE9b8cKIEfRIgo+Gs9mFlaD1WXsJ8wuF0HwPD8IRh1ArjeZQPNQTpU2kp7TRi7RUMSqt2xHpjthck+7I+ebbtKYE83nALWSCvEwGZhYPzI7tdrT5C0synwfdV5tudVwAPDsyGJRZWx05KOf0+SWI5/lhzBATrxNMn4LGTpztqFJkcPeLPAVsrIm2Wk6h/0R6NWeqWZI6j7lqYIiMKiVcNQqhThJ9bDmBnEVWaao45KjjA+C9I5MAPJnUWb7fn0Lfa4NvhQvgiw57HR/AW833GgVQM4NmWNKE9nZUOPGuptMD4F1UwzXBOtmBdwq9l7OxFk7P1t/k+ACeqLPJdCA+KNv329SbzY1ED7qzHR/AA+5s0wBuSM/7t0cYgIERsjIMQImeZLGxCECI0DEiApCCz3AlA0AANi2QDQBPwB7JADAG+Om4YACYhmpTrwgGgIUYmyoQALYiBALAZpxkANiOlg8ABzLyAeBIUj4AHMrLB4CyFPkAUJglHwBKE+UDQHGu+VbEw96oHkmiPD3RpnU55gpUReAFjQSbupoEUGHwkWiaiFeU+I5ItGlZpAFwZfSHrXcZmoaX9MKH4KcuWkGtq5GEwK+fvqm7owc92TcNTIz711QBAAAAAAAAAABwGzu2xPmLj8HiAAAAAElFTkSuQmCC
// ==/UserScript==

(function() {
    'use strict';

    let isResizing = false;
    let currentColumn = null;
    let startX = 0;
    let startWidth = 0;
    const columnWidths = new Map();

    // Tooltip element
    let tooltip = null;
    let tooltipTimeout = null;

    function createTooltip() {
        if (tooltip) return;

        tooltip = document.createElement('div');
        tooltip.className = 'contacts-tooltip';
        tooltip.style.cssText = `
            position: fixed;
            background: rgba(0, 0, 0, 0.85);
            color: white;
            padding: 8px 12px;
            border-radius: 4px;
            font-size: 13px;
            z-index: 10000;
            pointer-events: none;
            opacity: 0;
            transition: opacity 0.2s;
            max-width: 400px;
            word-wrap: break-word;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
        `;
        document.body.appendChild(tooltip);
    }

    function showTooltip(text, x, y) {
        if (!text || !tooltip) return;

        tooltip.textContent = text;
        tooltip.style.opacity = '0';
        tooltip.style.display = 'block';

        // Position tooltip
        const tooltipRect = tooltip.getBoundingClientRect();
        let left = x + 10;
        let top = y + 10;

        // Adjust if tooltip goes off screen
        if (left + tooltipRect.width > window.innerWidth) {
            left = x - tooltipRect.width - 10;
        }
        if (top + tooltipRect.height > window.innerHeight) {
            top = y - tooltipRect.height - 10;
        }

        tooltip.style.left = left + 'px';
        tooltip.style.top = top + 'px';
        tooltip.style.opacity = '1';
    }

    function hideTooltip() {
        if (tooltip) {
            tooltip.style.opacity = '0';
            setTimeout(() => {
                if (tooltip) tooltip.style.display = 'none';
            }, 200);
        }
        if (tooltipTimeout) {
            clearTimeout(tooltipTimeout);
            tooltipTimeout = null;
        }
    }

    function addTooltipListeners() {
        // Add tooltip to all data cells in .JcPRM columns
        const dataColumns = document.querySelectorAll('.pkxbt > .JcPRM');

        dataColumns.forEach(column => {
            if (column.dataset.tooltipAdded) return;
            column.dataset.tooltipAdded = 'true';

            column.addEventListener('mouseenter', (e) => {
                // Get text content from the column
                const textContent = column.textContent.trim();
                const isOverflowing = column.scrollWidth > column.clientWidth ||
                                     column.scrollHeight > column.clientHeight;

                if (textContent && (isOverflowing || textContent.length > 30)) {
                    tooltipTimeout = setTimeout(() => {
                        showTooltip(textContent, e.clientX, e.clientY);
                    }, 500);
                }
            });

            column.addEventListener('mousemove', (e) => {
                if (tooltip && tooltip.style.opacity === '1') {
                    const left = e.clientX + 10;
                    const top = e.clientY + 10;
                    tooltip.style.left = left + 'px';
                    tooltip.style.top = top + 'px';
                }
            });

            column.addEventListener('mouseleave', () => {
                hideTooltip();
            });
        });

        // Add tooltip to specific data cells
        const dataCells = document.querySelectorAll('.AYDrSb, .phtrWd');
        dataCells.forEach(cell => {
            if (cell.dataset.tooltipCellAdded) return;
            cell.dataset.tooltipCellAdded = 'true';

            cell.addEventListener('mouseenter', (e) => {
                const textContent = cell.textContent.trim();
                const isOverflowing = cell.scrollWidth > cell.clientWidth ||
                                     cell.scrollHeight > cell.clientHeight;

                if (textContent && (isOverflowing || textContent.length > 30)) {
                    tooltipTimeout = setTimeout(() => {
                        showTooltip(textContent, e.clientX, e.clientY);
                    }, 500);
                }
            });

            cell.addEventListener('mousemove', (e) => {
                if (tooltip && tooltip.style.opacity === '1') {
                    const left = e.clientX + 10;
                    const top = e.clientY + 10;
                    tooltip.style.left = left + 'px';
                    tooltip.style.top = top + 'px';
                }
            });

            cell.addEventListener('mouseleave', () => {
                hideTooltip();
            });
        });

        // Add tooltip to contact names in the main button
        const nameElements = document.querySelectorAll('.d5NbRd-EScbFb-JIbuQc');
        nameElements.forEach(nameEl => {
            if (nameEl.dataset.tooltipNameAdded) return;
            nameEl.dataset.tooltipNameAdded = 'true';

            const nameText = nameEl.getAttribute('aria-label');
            if (nameText) {
                nameEl.addEventListener('mouseenter', (e) => {
                    tooltipTimeout = setTimeout(() => {
                        showTooltip(nameText, e.clientX, e.clientY);
                    }, 500);
                });

                nameEl.addEventListener('mousemove', (e) => {
                    if (tooltip && tooltip.style.opacity === '1') {
                        const left = e.clientX + 10;
                        const top = e.clientY + 10;
                        tooltip.style.left = left + 'px';
                        tooltip.style.top = top + 'px';
                    }
                });

                nameEl.addEventListener('mouseleave', () => {
                    hideTooltip();
                });
            }
        });
    }

    function addColumnSeparators() {
        // Add separators to header columns
        const headerColumns = document.querySelectorAll('.ca2xib');
        headerColumns.forEach((column, index) => {
            if (column.querySelector('.column-separator')) return;

            const separator = document.createElement('div');
            separator.className = 'column-separator';
            separator.style.cssText = `
                position: absolute;
                right: 0;
                top: 0;
                width: 1px;
                height: 100%;
                background: rgba(0, 0, 0, 0.12);
                pointer-events: none;
                z-index: 1;
            `;
            column.style.position = 'relative';
            column.appendChild(separator);
        });

        // Add separators to data rows
        const dataRows = document.querySelectorAll('.pkxbt');
        dataRows.forEach(row => {
            const dataCells = row.querySelectorAll('.JcPRM');
            dataCells.forEach((cell, index) => {
                if (cell.querySelector('.column-separator')) return;

                const separator = document.createElement('div');
                separator.className = 'column-separator';
                separator.style.cssText = `
                    position: absolute;
                    right: 0;
                    top: 0;
                    width: 1px;
                    height: 100%;
                    background: rgba(0, 0, 0, 0.08);
                    pointer-events: none;
                    z-index: 1;
                `;
                cell.style.position = 'relative';
                cell.appendChild(separator);
            });
        });
    }

    function addResizeHandles() {
        const headerColumns = document.querySelectorAll('.ca2xib');

        headerColumns.forEach((column, index) => {
            if (column.querySelector('.resize-handle')) return;

            const handle = document.createElement('div');
            handle.className = 'resize-handle';
            handle.dataset.columnIndex = index;
            handle.style.cssText = `
                position: absolute;
                right: -2px;
                top: 0;
                width: 8px;
                height: 100%;
                cursor: col-resize;
                z-index: 1000;
                background: transparent;
            `;

            handle.addEventListener('mouseenter', () => {
                handle.style.background = 'rgba(66, 133, 244, 0.2)';
                handle.style.borderRight = '2px solid rgba(66, 133, 244, 0.6)';
            });

            handle.addEventListener('mouseleave', () => {
                if (!isResizing) {
                    handle.style.background = 'transparent';
                    handle.style.borderRight = 'none';
                }
            });

            handle.addEventListener('mousedown', (e) => {
                e.preventDefault();
                e.stopPropagation();
                isResizing = true;
                currentColumn = { element: column, index: index };
                startX = e.pageX;
                startWidth = column.offsetWidth;
                document.body.style.cursor = 'col-resize';
                document.body.style.userSelect = 'none';
                handle.style.background = 'rgba(66, 133, 244, 0.3)';
                handle.style.borderRight = '2px solid rgba(66, 133, 244, 0.8)';
            });

            column.style.position = 'relative';
            column.style.minWidth = '50px';
            column.appendChild(handle);
        });

        applyColumnWidths();
    }

    function applyColumnWidths() {
        columnWidths.forEach((width, index) => {
            // Apply to header
            const headerColumn = document.querySelectorAll('.ca2xib')[index];
            if (headerColumn) {
                headerColumn.style.width = width + 'px';
                headerColumn.style.minWidth = width + 'px';
                headerColumn.style.maxWidth = width + 'px';
                headerColumn.style.flexBasis = width + 'px';
                headerColumn.style.flexGrow = '0';
                headerColumn.style.flexShrink = '0';
            }

            // Apply to all data rows - target .pkxbt > .JcPRM
            const allDataColumns = document.querySelectorAll('.pkxbt > .JcPRM');
            allDataColumns.forEach(dataColumn => {
                const parent = dataColumn.parentElement;
                const siblings = Array.from(parent.children).filter(el => el.classList.contains('JcPRM'));
                const columnIndex = siblings.indexOf(dataColumn);

                if (columnIndex === index) {
                    dataColumn.style.width = width + 'px';
                    dataColumn.style.minWidth = width + 'px';
                    dataColumn.style.maxWidth = width + 'px';
                    dataColumn.style.flexBasis = width + 'px';
                    dataColumn.style.flexGrow = '0';
                    dataColumn.style.flexShrink = '0';
                    dataColumn.style.overflow = 'hidden';
                }
            });

            // Also apply to the checkbox columns
            const checkboxColumns = document.querySelectorAll('.Mqnmfe.JcPRM');
            checkboxColumns.forEach(col => {
                if (index === 0) {
                    col.style.width = width + 'px';
                    col.style.minWidth = width + 'px';
                    col.style.maxWidth = width + 'px';
                    col.style.flexBasis = width + 'px';
                    col.style.flexGrow = '0';
                    col.style.flexShrink = '0';
                }
            });
        });
    }

    function handleMouseMove(e) {
        if (!isResizing || !currentColumn) return;

        const diff = e.pageX - startX;
        const newWidth = Math.max(50, startWidth + diff);

        // Apply to header
        currentColumn.element.style.width = newWidth + 'px';
        currentColumn.element.style.minWidth = newWidth + 'px';
        currentColumn.element.style.maxWidth = newWidth + 'px';
        currentColumn.element.style.flexBasis = newWidth + 'px';
        currentColumn.element.style.flexGrow = '0';
        currentColumn.element.style.flexShrink = '0';

        columnWidths.set(currentColumn.index, newWidth);

        // Apply to all data columns - target .pkxbt > .JcPRM
        const allDataColumns = document.querySelectorAll('.pkxbt > .JcPRM');
        allDataColumns.forEach(dataColumn => {
            const parent = dataColumn.parentElement;
            const siblings = Array.from(parent.children).filter(el => el.classList.contains('JcPRM'));
            const columnIndex = siblings.indexOf(dataColumn);

            if (columnIndex === currentColumn.index) {
                dataColumn.style.width = newWidth + 'px';
                dataColumn.style.minWidth = newWidth + 'px';
                dataColumn.style.maxWidth = newWidth + 'px';
                dataColumn.style.flexBasis = newWidth + 'px';
                dataColumn.style.flexGrow = '0';
                dataColumn.style.flexShrink = '0';
                dataColumn.style.overflow = 'hidden';
            }
        });

        // Apply to checkbox columns if resizing first column
        if (currentColumn.index === 0) {
            const checkboxColumns = document.querySelectorAll('.Mqnmfe.JcPRM');
            checkboxColumns.forEach(col => {
                col.style.width = newWidth + 'px';
                col.style.minWidth = newWidth + 'px';
                col.style.maxWidth = newWidth + 'px';
                col.style.flexBasis = newWidth + 'px';
                col.style.flexGrow = '0';
                col.style.flexShrink = '0';
            });
        }
    }

    function handleMouseUp() {
        if (isResizing) {
            isResizing = false;
            currentColumn = null;
            document.body.style.cursor = '';
            document.body.style.userSelect = '';

            const handles = document.querySelectorAll('.resize-handle');
            handles.forEach(handle => {
                handle.style.background = 'transparent';
                handle.style.borderRight = 'none';
            });
        }
    }

    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);

    const observer = new MutationObserver((mutations) => {
        let shouldUpdate = false;
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length > 0) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1 && (
                        node.classList?.contains('XXcuqd') ||
                        node.classList?.contains('pkxbt') ||
                        node.querySelector?.('.pkxbt')
                    )) {
                        shouldUpdate = true;
                    }
                });
            }
        });

        if (shouldUpdate) {
            // addResizeHandles();
            applyColumnWidths();
            addColumnSeparators();
            addTooltipListeners();
        }
    });

    const init = () => {
        const headerRow = document.querySelector('.dFPtBe');
        if (headerRow) {
            createTooltip();
            // addResizeHandles();
            addColumnSeparators();
            applyColumnWidths();
            addTooltipListeners();
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        } else {
            setTimeout(init, 500);
        }
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();