GitHub Comment ID

Displays and allows copying of the comment ID on GitHub issue and pull request pages.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         GitHub Comment ID
// @namespace    https://github.com/Aiuanyu/GeminiChat2MD
// @version      0.4
// @description  Displays and allows copying of the comment ID on GitHub issue and pull request pages.
// @author       Aiuanyu
// @match        https://github.com/*/*/issues/*
// @match        https://github.com/*/*/pull/*
// @grant        none
// @license      MIT
// @history      0.4 Apply minor fixes from code review.
// @history      0.3 Fix UI overlap by injecting into a different element and using flexbox.
// @history      0.2 Add error handling for clipboard and adjust CSS
// @history      0.1 Initial release
// ==/UserScript==

(function() {
    'use strict';

    function addStyles() {
        const css = `
            .gh-comment-id-container {
                display: flex;
                align-items: center;
                font-size: 12px;
                color: var(--color-fg-muted);
                background-color: var(--color-canvas-subtle);
                border: 1px solid var(--color-border-default);
                border-radius: 6px;
                padding: 2px 6px;
                line-height: 1;
                margin-right: 8px; /* Space between ID and kebab menu */
            }
            .gh-comment-id-copy-button {
                background: none;
                border: none;
                cursor: pointer;
                margin-left: 5px;
                padding: 0;
                display: inline-flex;
                align-items: center;
                color: var(--color-fg-muted);
            }
            .gh-comment-id-copy-button:hover {
                color: var(--color-accent-fg);
            }
            .gh-comment-id-copy-button svg {
                fill: currentColor;
            }
        `;
        const styleSheet = document.createElement("style");
        styleSheet.innerText = css;
        document.head.appendChild(styleSheet);
    }

    function processComment(comment) {
        if (!comment.id || comment.querySelector('.gh-comment-id-container')) {
            return; // Skip if no ID or already processed
        }

        // GitHub comment IDs follow a format like "issuecomment-123456789"
        const commentId = comment.id.split('-').pop();
        if (!/^\d+$/.test(commentId)) {
            // Check if the commentId is purely numeric
            return;
        }

        const actionsContainer = comment.querySelector('.timeline-comment-actions');
        if (!actionsContainer) {
            return; // No actions container found
        }

        // Create the container
        const container = document.createElement('div');
        container.className = 'gh-comment-id-container';

        // Create the ID text span
        const idSpan = document.createElement('span');
        idSpan.textContent = commentId;
        container.appendChild(idSpan);

        // Create the copy button
        const copyButton = document.createElement('button');
        copyButton.className = 'gh-comment-id-copy-button';
        copyButton.title = 'Copy Comment ID';
        copyButton.innerHTML = `<svg aria-hidden="true" height="12" viewBox="0 0 16 16" version="1.1" width="12" data-view-component="true" class="octicon octicon-copy"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>`;

        if (!navigator.clipboard) {
            copyButton.title = 'Clipboard API not available';
            copyButton.disabled = true;
        } else {
            copyButton.onclick = (e) => {
                e.stopPropagation(); // prevent triggering other click events
                navigator.clipboard.writeText(commentId).then(() => {
                    copyButton.title = 'Copied!';
                    setTimeout(() => { copyButton.title = 'Copy Comment ID'; }, 2000);
                }).catch(err => {
                    console.error('Failed to copy comment ID:', err);
                    copyButton.title = 'Copy failed';
                    setTimeout(() => { copyButton.title = 'Copy Comment ID'; }, 2000);
                });
            };
        }
        container.appendChild(copyButton);

        // Inject the UI
        actionsContainer.prepend(container);
    }

    function processAllComments() {
        document.querySelectorAll('.timeline-comment-group').forEach(processComment);
    }

    function init() {
        addStyles();
        processAllComments();

        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                if (mutation.addedNodes.length) {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.matches('.timeline-comment-group')) {
                                processComment(node);
                            } else {
                                node.querySelectorAll('.timeline-comment-group').forEach(processComment);
                            }
                        }
                    });
                }
            }
        });

        const discussionContainer = document.querySelector('#discussion_bucket');
        if (discussionContainer) {
            observer.observe(discussionContainer, {
                childList: true,
                subtree: true
            });
        }
    }

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

})();