Stepik Invisible Character Button

Adds invisible character button to Stepik comment editor

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Stepik Invisible Character Button
// @namespace    https://stepik.org/
// @version      1.0
// @description  Adds invisible character button to Stepik comment editor
// @match        https://stepik.org/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const INVISIBLE_CHAR = '\u200B'; // Zero-width space

    function insertInvisibleChar() {
        const editor = document.querySelector('.rich-text-editor__content[contenteditable="true"]');
        if (!editor) return;

        // Insert invisible character at cursor position
        const selection = window.getSelection();
        const range = selection.getRangeAt(0);

        const textNode = document.createTextNode(INVISIBLE_CHAR);
        range.insertNode(textNode);

        // Move cursor after inserted character
        range.setStartAfter(textNode);
        range.setEndAfter(textNode);
        selection.removeAllRanges();
        selection.addRange(range);

        // Trigger input events to enable submit button
        editor.dispatchEvent(new Event('input', { bubbles: true }));
        editor.dispatchEvent(new Event('change', { bubbles: true }));
        editor.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));

        // Focus editor
        editor.focus();
    }

    function createButton() {
        const btn = document.createElement('a');
        btn.id = 'invisible-char-btn';
        btn.className = 'cke_button cke_button_off';
        btn.title = 'Insert Invisible Character';
        btn.tabIndex = '-1';
        btn.setAttribute('role', 'button');
        btn.style.cursor = 'pointer';

        const icon = document.createElement('span');
        icon.className = 'cke_button_icon';
        icon.style.cssText = 'background: none; width: 16px; height: 16px; display: inline-block;';
        icon.innerHTML = '⧉';
        icon.style.fontSize = '16px';
        icon.style.lineHeight = '16px';

        const label = document.createElement('span');
        label.className = 'cke_button_label';
        label.textContent = 'Invisible Char';
        label.style.display = 'none';

        btn.appendChild(icon);
        btn.appendChild(label);

        btn.addEventListener('click', (e) => {
            e.preventDefault();
            insertInvisibleChar();
        });

        return btn;
    }

    function addButton() {
        const toolgroup = document.querySelector('.cke_toolgroup');
        if (!toolgroup) {
            setTimeout(addButton, 500);
            return;
        }

        if (document.getElementById('invisible-char-btn')) return;

        const btn = createButton();
        toolgroup.appendChild(btn);
    }

    // Wait for editor to load
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => setTimeout(addButton, 1000));
    } else {
        setTimeout(addButton, 1000);
    }

    // Re-add button if comment widget reloads
    const observer = new MutationObserver(() => {
        if (document.querySelector('.cke_toolgroup') && !document.getElementById('invisible-char-btn')) {
            addButton();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();