Greasy Fork is available in English.

JIRA Copy

Copy Copy Copy!

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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
    });
})();