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