// ==UserScript==
// @name 云效塞满Emoji
// @name:en Yunxiao Full Emoji
// @name:zh-cn 云效塞满Emoji
// @namespace com.ui-ceiling.yoho.title-emoji
// @version 1.1.3
// @description 云效创建/编辑 需求/任务时 标题允许输入Emoji
// @description:zh-cn 允许在云效标题中输入 Emoji 表情
// @author UI-ceiling
// @match https://devops.aliyun.com/*
// @icon https://www.emojiall.com/images/60/microsoft-teams/1f923.png
// @license MIT
// ==/UserScript==
(() => {
'use strict';
const NEW_INPUT_ID = 'emojiOverrideInput';
const ORIG_INPUT_ID = 'workitemTitleInputBox';
const URL_HOOK_DELAY = 1000;
/** 监听 URL 路由变化 */
const onUrlChange = (callback) => {
let lastUrl = location.href;
const wrap = (method) => {
const origin = history[method];
history[method] = function (...args) {
const result = origin.apply(this, args);
if (location.href !== lastUrl) {
lastUrl = location.href;
callback(location.href);
}
return result;
};
};
['pushState', 'replaceState'].forEach(wrap);
window.addEventListener('popstate', () => {
if (location.href !== lastUrl) {
lastUrl = location.href;
callback(location.href);
}
});
};
/** 等待原输入框出现 */
const waitForOriginalInput = () =>
new Promise((resolve) => {
const check = () => document.getElementById(ORIG_INPUT_ID);
const input = check();
if (input) return resolve(input);
const observer = new MutationObserver(() => {
const input = check();
if (input) {
observer.disconnect();
resolve(input);
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
/** 模拟 React 内部输入变更 */
function simulateReactInput(inputEl) {
const lastValue = inputEl.value;
inputEl.value = new Date().getTime();
const tracker = inputEl._valueTracker;
if (tracker) {
tracker.setValue(lastValue); // 告诉 React:值变了
}
const inputEvent = new Event('input', { bubbles: true });
inputEl.dispatchEvent(inputEvent);
}
/** 注入 emoji 输入框 */
const injectNewInput = (origInput) => {
if (!origInput || document.getElementById(NEW_INPUT_ID)) return;
const container = document.createElement('div');
container.style.position = 'relative';
const tagName = origInput.tagName.toLowerCase(); // 'input' 或 'textarea'
const newInput = document.createElement(tagName);
Object.assign(newInput, {
id: NEW_INPUT_ID,
value: origInput.value,
placeholder: '请输入标题',
className: origInput.className,
});
newInput.style.cssText = origInput.style.cssText;
// 美化 emoji 图标
const emoji = document.createElement('span');
emoji.textContent = '✨';
emoji.className = 'emoji-decorator';
Object.assign(emoji.style, {
position: 'absolute',
right: '8px',
top: '50%',
transform: 'translateY(-50%)',
fontSize: '18px',
pointerEvents: 'none',
userSelect: 'none',
animation: 'emoji-pop 0.5s ease-out',
});
// 动画样式(只注入一次)
if (!document.getElementById('emoji-style')) {
const style = document.createElement('style');
style.id = 'emoji-style';
style.textContent = `
@keyframes emoji-pop {
0% { transform: translateY(-50%) scale(0.6); opacity: 0; }
40% { transform: translateY(-50%) scale(2); opacity: 1; }
100% { transform: translateY(-50%) scale(1); }
}
`;
document.head.appendChild(style);
}
// padding 防遮挡
const padRight = parseFloat(getComputedStyle(newInput).paddingRight) || 0;
if (padRight < 28) newInput.style.paddingRight = '28px';
newInput.addEventListener('blur', () => {
const newVal = newInput.value.trim();
const oldVal = origInput.value.trim();
if (!newVal || newVal === oldVal) return; // 相同就不触发更新
// 模拟用户输入,更新原文本框
simulateReactInput(origInput);
// 触发原文本框的失焦事件
const blurEvent = new Event('blur', { bubbles: true });
origInput.dispatchEvent(blurEvent);
});
container.append(newInput, emoji);
origInput.style.display = 'none';
origInput.parentElement?.appendChild(container);
};
/** 显示提示 */
const showToast = (message, duration = 3000) => {
const old = document.getElementById('emoji-toast');
if (old) {
old.remove(); // 强制移除旧吐司,避免堆叠
}
const toast = document.createElement('div');
Object.assign(toast, {
id: 'emoji-toast',
textContent: message,
});
Object.assign(toast.style, {
position: 'fixed',
top: '10px',
left: '50%',
transform: 'translateX(-50%)',
backgroundColor: 'rgba(0,0,0,0.7)',
color: '#fff',
padding: '10px 20px',
borderRadius: '20px',
fontSize: '34px',
zIndex: 9999,
opacity: '0',
transition: 'opacity 0.3s ease',
pointerEvents: 'none',
userSelect: 'none',
});
document.body.appendChild(toast);
requestAnimationFrame(() => (toast.style.opacity = '1'));
setTimeout(() => {
toast.style.opacity = '0';
toast.addEventListener('transitionend', () => toast.remove());
}, duration);
};
/** 初始化入口 */
const initInject = async () => {
try {
const origInput = await waitForOriginalInput();
injectNewInput(origInput);
} catch (err) {
console.warn('[Tampermonkey] emoji input 注入失败:', err);
}
};
// 首次加载
setTimeout(initInject, URL_HOOK_DELAY);
// 路由变化监听
let injectTimer = null;
onUrlChange(() => {
clearTimeout(injectTimer);
injectTimer = setTimeout(initInject, URL_HOOK_DELAY);
});
/** 覆盖 PATCH 请求的值 */
const rawOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (...args) {
[this._method, this._url] = args;
return rawOpen.apply(this, args);
};
const rawSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (body) {
try {
const method = this._method?.toUpperCase();
if (
(method === 'PATCH' || method === 'POST') &&
this._url?.includes('projex/api/workitem/workitem')
) {
const parsed = JSON.parse(body);
const override = document.getElementById(NEW_INPUT_ID)?.value?.trim();
if (override) {
console.log('[Tampermonkey] 已覆盖 propertyValue:', override);
if(method === 'PATCH') {
parsed.propertyValue = override;
}else{
parsed.subject = override;
}
body = JSON.stringify(parsed);
}
}
} catch (e) {
// 非 JSON 请求忽略
}
return rawSend.call(this, body);
};
window.addEventListener('keydown', (e) => {
const isMac = navigator.platform.toUpperCase().includes('MAC');
const isCtrl = isMac ? e.metaKey : e.ctrlKey;
if (isCtrl && e.shiftKey && e.altKey && e.key.toLowerCase() === 'e') {
e.preventDefault();
showToast('⌛️ 手动注入 !');
initInject().then(() => {
showToast('🤣 Emoji 输入框注入成功!');
}).catch(() => {
showToast('❌ 注入失败,请检查元素是否存在');
});
}
});
const observeInputRemoval = () => {
let hasAppeared = false;
let reInjecting = false;
const observer = new MutationObserver(() => {
const input = document.getElementById(NEW_INPUT_ID);
if (input) {
hasAppeared = true;
reInjecting = false;
return; // 一切正常
}
// 若已出现过但现在被移除,触发注入(节流避免过度触发)
if (hasAppeared && !reInjecting) {
reInjecting = true;
console.log('⌛️ emoji 输入框被移除,尝试重新注入...');
// showToast('⚠️ Emoji 输入框被移除,尝试恢复...');
initInject().finally(() => {
setTimeout(() => (reInjecting = false), 1000); // 1秒节流
});
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
};
observeInputRemoval(); // 启动输入框丢失监听
})();