JIRA Copy

Copy Copy Copy!

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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
    });
})();