JIRA Copy

Copy Copy Copy!

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.

Tendrás que 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.

Tendrás que instalar una extensión como Tampermonkey antes de poder 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)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

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

// ==UserScript==
// @name         JIRA Copy
// @namespace    http://tampermonkey.net/
// @version      0.7.2
// @description  Copy Copy Copy!
// @author       alex4814 & Tabbit
// @match        http://jira.sanguosha.com:8080/browse/*
// @match        http://jira.sanguosha.com:8080/secure/RapidBoard.jspa*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=sanguosha.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        btnText: '复制单号标题',
        successText: '复制成功',
        feedbackTime: 1500,
        iconClass: 'aui-icon aui-icon-small aui-iconfont-copy',
        singleTopBtnId: 'copy-jira-single',
        boardCardBtnClass: 'tm-copy-card-inline',
        boardDetailBtnClass: 'tm-copy-detail-inline',
        inlineWrapClass: 'tm-copy-inline-wrap'
    };

    function copyToClipboard(text, btnElement) {
        if (!text) return;

        const performFeedback = () => {
            if (!btnElement) return;

            const oldTitle = btnElement.getAttribute('title') || '';
            btnElement.setAttribute('title', CONFIG.successText);
            btnElement.classList.add('tm-copy-success');

            const label = btnElement.querySelector('.btn-label');
            let originalText = '';
            if (label) {
                originalText = label.innerText;
                label.innerText = CONFIG.successText;
            }

            setTimeout(() => {
                btnElement.setAttribute('title', oldTitle);
                btnElement.classList.remove('tm-copy-success');
                if (label) label.innerText = originalText;
            }, CONFIG.feedbackTime);
        };

        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(text).then(performFeedback).catch(err => {
                console.warn('现代复制 API 失败,尝试备选方案', err);
                fallbackCopy(text, performFeedback);
            });
        } else {
            fallbackCopy(text, performFeedback);
        }
    }

    function fallbackCopy(text, callback) {
        const textArea = document.createElement('textarea');
        textArea.value = text;
        textArea.style.position = 'fixed';
        textArea.style.left = '-9999px';
        textArea.style.top = '0';
        document.body.appendChild(textArea);
        textArea.focus();
        textArea.select();

        try {
            const successful = document.execCommand('copy');
            if (successful) callback();
        } catch (err) {
            console.error('无法复制文本', err);
        }

        document.body.removeChild(textArea);
    }

    function formatCopyText(key, summary) {
        return `#${key} ${summary}`;
    }

    function injectStyles() {
        if (document.getElementById('tm-copy-style')) return;

        const style = document.createElement('style');
        style.id = 'tm-copy-style';
        style.textContent = `
            .${CONFIG.inlineWrapClass} {
                display: inline-flex;
                align-items: center;
                vertical-align: middle;
                line-height: 1;
            }

            .tm-copy-icon-btn {
                display: inline-flex;
                align-items: center;
                justify-content: center;
                width: 16px;
                height: 16px;
                margin-left: 4px;
                padding: 0;
                border: none;
                background: transparent;
                cursor: pointer;
                color: #6B778C;
                vertical-align: middle;
                position: relative;
                top: 0;
            }

            .tm-copy-icon-btn:hover {
                color: #0052CC;
            }

            .tm-copy-icon-btn.tm-copy-success {
                color: #36B37E;
            }

            .tm-copy-icon-btn .aui-icon {
                margin: 0;
                width: 16px;
                height: 16px;
                line-height: 16px;
                display: inline-block;
            }
        `;
        document.head.appendChild(style);
    }

    function createInlineIconButton(className, title) {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = `tm-copy-icon-btn ${className}`;
        btn.title = title || '复制单号和标题';

        const icon = document.createElement('span');
        icon.className = CONFIG.iconClass;
        btn.appendChild(icon);

        return btn;
    }

    function createNormalButton(text) {
        const btn = document.createElement('button');
        btn.className = 'aui-button';

        const icon = document.createElement('span');
        icon.className = CONFIG.iconClass;
        icon.style.marginRight = '4px';

        const label = document.createElement('span');
        label.className = 'btn-label';
        label.innerText = text;

        btn.appendChild(icon);
        btn.appendChild(label);
        return btn;
    }

    function wrapKeyWithInlineContainer(keyEl, btnClass) {
        if (!keyEl) return null;

        const parent = keyEl.parentElement;
        if (!parent) return null;

        if (parent.classList.contains(CONFIG.inlineWrapClass)) {
            if (parent.querySelector(`.${btnClass}`)) return null;
            return parent;
        }

        const wrapper = document.createElement('span');
        wrapper.className = CONFIG.inlineWrapClass;

        parent.insertBefore(wrapper, keyEl);
        wrapper.appendChild(keyEl);

        return wrapper;
    }

    function initSingleViewTopButton() {
        const target = document.querySelector('#stalker .aui-toolbar2-primary');
        if (!target || document.getElementById(CONFIG.singleTopBtnId)) return;

        const wrapper = document.createElement('div');
        wrapper.className = 'aui-buttons pluggable-ops';

        const btn = createNormalButton(CONFIG.btnText);
        btn.id = CONFIG.singleTopBtnId;

        btn.onclick = () => {
            const key = document.getElementById('key-val')?.innerText.trim();
            const summary = document.getElementById('summary-val')?.innerText.trim();
            if (key && summary) {
                copyToClipboard(formatCopyText(key, summary), btn);
            }
        };

        wrapper.appendChild(btn);
        target.prepend(wrapper);
    }

    function initBoardCards() {
        const issues = document.querySelectorAll('.ghx-issue');

        issues.forEach(issue => {
            const keyEl = issue.querySelector('.ghx-key');
            if (!keyEl) return;

            const wrapper = wrapKeyWithInlineContainer(keyEl, CONFIG.boardCardBtnClass);
            if (!wrapper) return;

            const btn = createInlineIconButton(CONFIG.boardCardBtnClass, '复制单号和标题');

            btn.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();

                const key = issue.querySelector('.ghx-key')?.innerText.trim();
                const summary = issue.querySelector('.ghx-summary')?.innerText.trim();
                if (key && summary) {
                    copyToClipboard(formatCopyText(key, summary), btn);
                }
            });

            wrapper.appendChild(btn);
        });
    }

    function initBoardDetailPanel() {
        const detailRoot =
            document.querySelector('#ghx-detail-view') ||
            document.querySelector('.ghx-detail-view') ||
            document.querySelector('[data-testid="issue-detail-view"]');

        if (!detailRoot) return;

        const keyEl =
            detailRoot.querySelector('#key-val') ||
            detailRoot.querySelector('.issue-link-key') ||
            detailRoot.querySelector('[data-issue-key]');

        const summaryEl =
            detailRoot.querySelector('#summary-val') ||
            detailRoot.querySelector('#ghx-detail-summary') ||
            detailRoot.querySelector('.ghx-detail-view .jira-issue-header-content h2') ||
            detailRoot.querySelector('[data-testid="issue.views.issue-base.foundation.summary.heading"]');

        if (!keyEl || !summaryEl) return;

        const wrapper = wrapKeyWithInlineContainer(keyEl, CONFIG.boardDetailBtnClass);
        if (!wrapper) return;

        const btn = createInlineIconButton(CONFIG.boardDetailBtnClass, '复制单号和标题');

        btn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();

            const key = keyEl.innerText?.trim();
            const summary = summaryEl.innerText?.trim();
            if (key && summary) {
                copyToClipboard(formatCopyText(key, summary), btn);
            }
        });

        wrapper.appendChild(btn);
    }

    function init() {
        injectStyles();

        const url = window.location.href;
        if (url.includes('/browse/')) {
            initSingleViewTopButton();
        } else if (url.includes('RapidBoard.jspa')) {
            initBoardCards();
            initBoardDetailPanel();
        }
    }

    let timer = null;
    function scheduleInit() {
        clearTimeout(timer);
        timer = setTimeout(init, 120);
    }

    init();

    const observer = new MutationObserver(() => {
        scheduleInit();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();