Skip to content

Instantly share code, notes, and snippets.

@nazarewk
Last active May 13, 2025 11:36
Show Gist options
  • Save nazarewk/adb1d31e66c2b40b7ea8c4a11a53ce12 to your computer and use it in GitHub Desktop.
Save nazarewk/adb1d31e66c2b40b7ea8c4a11a53ce12 to your computer and use it in GitHub Desktop.
Using TamperMonkey on Firefox to set up TinyMCE rich text editor with Zammad helpdesk software

TinyMCE in Zammad text boxes

This is barely working proof of concept that just confirms it's possible to use TinyMCE in Zammad using either browser console or TamperMonkey user scripts.

It was created with some help from the awemsome Kagi Assistant / Claude 3.7 Sonnet (never wrote/used user scripts in browsers or the TinyMCE itself, didn't write Javascript for a decade). Developed mostly in Firefox's browser console before finally wrapping it up in TamperMonkey script.

It adds 3 custom shortcuts:

// Register keyboard shortcuts
editor.shortcuts.add('alt+m', 'Monospace', toggleMonospace);
editor.shortcuts.add('meta+alt+m', 'Code', toggleCode);
editor.shortcuts.add('meta+shift+c', 'Pre Block', togglePre);

looks like this: image

Quick start

Disable Zammad shortcuts completely (didn't check whether they clash at all).

Add the zammad-tinymce.js to TamperMonkey scripts

Set up your instance's domain at script edition > Settings tab > Includes/Excludes > User Matches list (middle column). The entry format will be like https://zammad.example.com/*.

Adjust Content Security Policy on your instance to allow downloading scripts and styles from the CDN: there will be a lot plugin files/styles downloaded. In Caddyfile you could do it by regex-replacing script-src and style-src CSP directives like this:

header {
  >Content-Security-Policy "script-src ([^;]*)" "script-src $1 https://cdn.jsdelivr.net"
  >Content-Security-Policy "style-src ([^;]*)" "style-src $1 https://cdn.jsdelivr.net"
}

Known issues

  • it should probably render a full editor, instead of popup above the div, but is still very useful
    • the pop-up does not "stick" to the <div>: stays in the same place on the screen while scrolling the page

TODO

  • use a DOM element watcher instead of setInterval()

Resources

// ==UserScript==
// @name TinyMCE Editor for Zammad Helpdesk
// @namespace https://github.com/nazarewk
// @version 1.0
// @description Adds TinyMCE editor to rich text areas in Zammad
// @homepage https://gist.github.com/nazarewk/adb1d31e66c2b40b7ea8c4a11a53ce12
// @author nazarewk
// @grant GM_addElement
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
const tinyMCEVersion = "7.8";
const markerClass = "kdn-mce-initialized";
const selector = `[contenteditable="true"].richtext-content:not(.${markerClass})`;
window.kdnTinyMCE = {};
const self = window.kdnTinyMCE;
function tryInitializeOn(element) {
if (typeof tinymce === undefined) {
console.error("tinymce variable is not loaded yet!");
return false;
}
if (element.classList.contains(markerClass)) {
return true;
} else {
element.classList.add(markerClass);
}
tinymce.init({
target: element,
inline: true,
skin: (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'oxide-dark' : 'oxide'),
content_css: (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'),
plugins: [
"advlist", "anchor", "autolink", "charmap", "code", "fullscreen",
"help", "image", "insertdatetime", "link", "lists", "media",
"preview", "searchreplace", "table", "visualblocks",
],
toolbar: [
"alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image",
"undo redo | styles | bold italic underline strikethrough monospace code-fmt | blockquote pre-fmt",
],
setup: (editor) => {
const toggleMonospace = () => editor.execCommand('mceToggleFormat', false, 'monospace');
const toggleCode = () => editor.execCommand('mceToggleFormat', false, 'code');
const togglePre = () => editor.execCommand('mceToggleFormat', false, 'pre');
// Add Monospace button
editor.ui.registry.addButton('monospace', {
text: 'M',
tooltip: 'Monospace',
shortcut: 'alt+m',
onAction: toggleMonospace
});
// Add Code button
editor.ui.registry.addButton('code-fmt', {
text: '</>',
tooltip: 'Code',
shortcut: 'meta+alt+m',
onAction: toggleCode
});
// Add Pre button
editor.ui.registry.addButton('pre-fmt', {
text: 'PRE',
tooltip: 'Preformatted Block (⌘⇧C)',
onAction: togglePre
});
// Register keyboard shortcuts
editor.shortcuts.add('alt+m', 'Monospace', toggleMonospace);
editor.shortcuts.add('meta+alt+m', 'Code', toggleCode);
editor.shortcuts.add('meta+shift+c', 'Pre Block', togglePre);
},
formats: {
monospace: {inline: 'span', styles: {fontFamily: 'monospace'}},
code: {inline: 'code'},
pre: {block: 'pre', remove: 'all'}
}
});
return true;
}
function discover() {
const elements = document.querySelectorAll(selector);
self.queue.push(...elements);
};
function tick() {
if (self.isProcessing) {
console.log("already processing");
return false;
}
self.isProcessing = true;
self.discover();
self.process();
self.isProcessing = false;
return true;
}
function process() {
let emptyLength = 0;
while (self.queue.length > emptyLength) {
const element = self.queue.shift();
if (!self.tryInitializeOn(element)) {
self.queue.push(element);
emptyLength += 1;
}
}
}
const script = GM_addElement('script', {
src: `https://cdn.jsdelivr.net/npm/tinymce@${tinyMCEVersion}/tinymce.min.js`,
type: "text/javascript",
});
self.queue = [];
self.discover = discover;
self.process = process;
self.isProcessing = false;
self.tick = tick;
self.tryInitializeOn = tryInitializeOn;
self.tickerId = null;
script.addEventListener("load", function () {
self.tickerId = setInterval(tick, 1000);
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment