Google 通訊錄 - 可調整欄寬與工具提示

透過拖曳使 Google 通訊錄表格欄寬可調整,懸停時顯示完整內容

目前為 2025-12-29 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Google Contacts - Adjustable Column Width with Tooltips
// @name:zh-CN   Google 通讯录 - 可调整列宽与工具提示
// @name:zh-TW   Google 通訊錄 - 可調整欄寬與工具提示
// @name:ja      Google 連絡先 - 調整可能な列幅とツールチップ
// @name:ko      Google 연락처 - 조정 가능한 열 너비 및 도구 설명
// @name:de      Google Kontos - Anpassbare Spaltenbreite mit Tooltips
// @name:fr      Google Contacts - Largeur de colonne adjustable avec info-bulles
// @name:es      Google Contacts - Ancho de columna ajustable con información sobre herramientas
// @name:it      Google Contatti - Larghezza colonna regolabile con suggerimenti
// @name:pt      Google Contacts - Largura de coluna ajustável com dicas de ferramenta
// @name:ru      Google Контакты - Настраиваемая ширина столбца с подсказками
// @name:ar      جهات اتصال Google - عرض عمود قابل للتعديل مع تلميحات أدوات
// @name:nl      Google Contacten - Aanpasbare kolombreedte met knopinfo
// @name:pl      Google Contacts - Regulowana szerokość kolumny z etykietkami narzędzi
// @name:tr      Google Kişiler - Araç İpuçlarıyla Ayarlanabilir Sütun Genişliği
// @name:vi      Google Danh bạ - Chiều rộng cột có thể điều chỉnh với chú thích công cụ
// @name:th      Google รายชื่อติดต่อ - ความกว้างคอลัมน์ปรับได้พร้อมเครื่องมือแนะนำ
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Make Google Contacts table columns resizable by dragging and show full content on hover
// @description:zh-CN  通过拖拽使 Google 通讯录表格列宽可调整,悬停时显示完整内容
// @description:zh-TW  透過拖曳使 Google 通訊錄表格欄寬可調整,懸停時顯示完整內容
// @description:ja     Google連絡先のテーブル列をドラッグでサイズ変更可能にし、ホバー時に全文を表示
// @description:ko     Google 연락처 테이블 열을 드래그로 크기 조절 가능하게 하고 호버 시 전체 내용 표시
// @description:de     Google Kontakte-Tabellenspalten durch Ziehen in der Größe anpassbar machen und vollen Inhalt beim Hovern anzeigen
// @description:fr     Rendre les colonnes du tableau Google Contacts redimensionnables par glisser-déposer et afficher le contenu complet au survol
// @description:es     Hacer que las columnas de la tabla de Google Contacts se puedan redimensionar arrastrando y mostrar el contenido completo al pasar el mouse
// @description:it     Rende ridimensionabili le colonne della tabella di Google Contatti trascinando e mostra il contenuto completo al passaggio del mouse
// @description:pt     Tornar as colunas da tabela do Google Contacts redimensionáveis por arrastar e mostrar o conteúdo completo ao passar o mouse
// @description:ru     Сделать столбцы таблицы Google Контакты изменяемыми путем перетаскивания и отображать полное содержимое при наведении
// @description:ar     جعل أعمدة جدول جهات اتصال Google قابلة لتغيير الحجم بالسحب وإظهار المحتوى الكامل عند التمرير
// @description:nl     Maak kolommen van de Google Contacten-tabel aanpasbaar door slepen en toon de volledige inhoud bij het aanwijzen
// @description:pl     Spraw, aby kolumny tabeli Google Contacts można było zmieniać rozmiar przez przeciąganie i wyświetlać pełną zawartość po najechaniu myszką
// @description:tr     Google Kişiler tablo sütunlarını sürükleme ile yeniden boyutlandırılabilir yapın ve üzerine gelindiğinde tam içeriği gösterin
// @description:vi     Làm cho các cột bảng Google Danh bạ có thể thay đổi kích thước bằng cách kéo và hiển thị đầy đủ nội dung khi di chuột qua
// @description:th     ทำให้คอลัมน์ตาราง Google รายชื่อติดต่อปรับขนาดได้ด้วยการลากและแสดงเนื้อหาทั้งหมดเมื่อวางเมาส์
// @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();
    }
})();