JIRA Copy

Copy Copy Copy!

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

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