Last active
February 23, 2026 09:46
-
-
Save tnodet/0f4d85ed251b6b977ddd8197f64488d2 to your computer and use it in GitHub Desktop.
A simple Tampermonkey script to hide the annoying emoji / emoticon popup that appears in Jira when you type a colon (":")
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Jira - Hide Emoji / Emoticon Typeahead Popup | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2.0 | |
| // @description Hides the emoji / emoticon popup when typing a colon (":") in Jira | |
| // @author Tanguy Nodet | |
| // @match https://*.atlassian.net/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=jira.atlassian.com | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| // ----------------------------------------------------------------------------------- | |
| // 1️⃣ Hide emoji popups | |
| // ----------------------------------------------------------------------------------- | |
| const popupObserver = new MutationObserver((mutations) => { | |
| for (const mutation of mutations) { | |
| for (const node of mutation.addedNodes) { | |
| if (!(node instanceof HTMLElement)) continue; | |
| // Only target editor popups | |
| if ( | |
| node.tagName === "DIV" && | |
| node.getAttribute("data-editor-popup") === "true" && | |
| node.getAttribute("data-testid") === "popup-wrapper" | |
| ) { | |
| // Check if any item in the popup has a data-emoji-id attribute | |
| const hasEmojiItems = node.querySelector("[data-emoji-id]") !== null; | |
| if (hasEmojiItems) { | |
| // We hide the popup instead of removing it to avoid an error later on when | |
| // the page tries to remove it itself. | |
| node.style.display = "none"; | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| popupObserver.observe(document.body, { childList: true, subtree: true }); | |
| // ----------------------------------------------------------------------------------- | |
| // 2️⃣ Neutralize <mark> node style (blue text) | |
| // ----------------------------------------------------------------------------------- | |
| const style = document.createElement("style"); | |
| style.textContent = ` | |
| mark[data-type-ahead-query="true"][data-trigger=":"] { | |
| color: inherit !important; | |
| background: none !important; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| // ----------------------------------------------------------------------------------- | |
| // Helper: move the cursor to the end of a text node | |
| // ----------------------------------------------------------------------------------- | |
| function moveCursorToEndOf(textNode) { | |
| const selection = window.getSelection(); | |
| const range = document.createRange(); | |
| range.setStart(textNode, textNode.length); | |
| range.collapse(true); | |
| selection.removeAllRanges(); | |
| selection.addRange(range); | |
| } | |
| const ZWS = "\u200B"; // zero-width space character | |
| // ----------------------------------------------------------------------------------- | |
| // 3️⃣ Bypass the typeahead logic by replacing the <mark> node | |
| // ----------------------------------------------------------------------------------- | |
| // This is the most tricky part: | |
| // - We can't just replace the <mark> node with a ":" text node, because Jira's | |
| // typeahead logic will detect it and re-create the <mark> node, creating an | |
| // infinite loop of mutations. | |
| // - Instead, we replace the <mark> node with a ":" + ZWS text node. This way, the | |
| // typeahead logic is somehow bypassed and doesn't re-create the <mark> node. | |
| // This could work with other characters, but ZWS is a good candidate because it's | |
| // invisible, so the user doesn't see it. | |
| // - We can't cleanup the ZWS character immediately because the typeahead logic would | |
| // re-create the <mark> node if it detects the ":" character alone, triggering the | |
| // infinite loop. Instead, we wait for the next tick to cleanup the ZWS character. | |
| // - The complete fix goes like this: | |
| // 1. Detect <mark> node creation and replace it with ":" + ZWS text node | |
| // 2. Move cursor after the ZWS character | |
| // 3. In the next tick, cleanup the ZWS character from the text node | |
| // 4. Move cursor at the end of the current text node | |
| const markObserver = new MutationObserver((mutations) => { | |
| for (const mutation of mutations) { | |
| for (const node of mutation.addedNodes) { | |
| if (!(node instanceof HTMLElement)) continue; | |
| if ( | |
| node.tagName === "MARK" && | |
| node.getAttribute("data-type-ahead-query") === "true" && | |
| node.getAttribute("data-trigger") === ":" | |
| ) { | |
| const parent = node.parentNode; | |
| if (!parent) return; | |
| // Replace <mark> node with a TextNode containing colon + ZWS | |
| const textNode = document.createTextNode(":" + ZWS); | |
| node.replaceWith(textNode); | |
| // Move cursor after ZWS | |
| moveCursorToEndOf(textNode); | |
| setTimeout(() => { | |
| // Cleanup ZWS inside parent | |
| const walker = document.createTreeWalker( | |
| parent, | |
| NodeFilter.SHOW_TEXT, | |
| null, | |
| false, | |
| ); | |
| // Find the current text node that contains the ZWS and remove it | |
| let current; | |
| while ((current = walker.nextNode())) { | |
| if (current.nodeValue.includes(ZWS)) { | |
| current.nodeValue = current.nodeValue.replace(ZWS, ""); | |
| break; // only ever one ZWS | |
| } | |
| } | |
| // Move cursor at end of current node | |
| moveCursorToEndOf(current); | |
| }, 0); | |
| } | |
| } | |
| } | |
| }); | |
| markObserver.observe(document.body, { childList: true, subtree: true }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment