JIRA Copy

Copy Copy Copy!

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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