Text Editor - Invisible Characters Plugin

Toggle visible markers for spaces and paragraph breaks.

#Usage

import { TextEditor } from '@primeui/vue-texteditor';
import { invisibleCharsPlugin } from './invisible-chars-plugin';
<TextEditor.Root v-model="value" :plugins="[invisibleCharsPlugin]">
    <TextEditor.Toolbar v-slot="{ plugins }">
        <button @click="plugins.invisibleChars?.toggle()">Invisible Characters</button>
    </TextEditor.Toolbar>
    <TextEditor.Content />
</TextEditor.Root>

The Invisible Characters plugin renders a · for each space and a at the end of each text block, making whitespace and paragraph boundaries visible. The markers are widget decorations held in the plugin's own state via a meta transaction and never enter the document, so the value stays clean. The toggle command shows and hides them.

#Commands

CommandSignatureDescription
show() => voidShow the markers
hide() => voidHide the markers
toggle() => voidToggle marker visibility
isEnabled() => booleanWhether the markers are currently shown

#Plugin Definition

import { defineTextEditorPlugin } from '@primeui/vue-texteditor';
import { Plugin, PluginKey } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view';

const key = new PluginKey('invisibleChars');

// buildDecorations(state) walks the doc, adding a widget '·' after each space
// and a '¶' at the end of every text block, then returns a DecorationSet.

export const invisibleCharsPlugin = defineTextEditorPlugin('invisibleChars', (ctx) => {
    const remove = ctx.registerProseMirrorPlugin(
        new Plugin({
            key,
            state: {
                init: () => ({ enabled: false, decorations: DecorationSet.empty }),
                apply(tr, value, _old, newState) {
                    const toggle = tr.getMeta(key);
                    const enabled = toggle ?? value.enabled;

                    if (!enabled) return { enabled, decorations: DecorationSet.empty };
                    if (toggle === true || (tr.docChanged && value.enabled)) {
                        return { enabled, decorations: buildDecorations(newState) };
                    }

                    return { enabled, decorations: value.decorations.map(tr.mapping, tr.doc) };
                }
            },
            props: { decorations: (state) => key.getState(state)?.decorations }
        })
    );

    ctx.onUnmounted(remove);

    return {
        commands: {
            toggle: () => {
                const view = ctx.getView();
                if (!view) return;
                const on = key.getState(view.state)?.enabled ?? false;
                view.dispatch(view.state.tr.setMeta(key, !on));
            }
            // show / hide / isEnabled omitted for brevity
        }
    };
});

#Example

Click Toggle Invisible Characters to reveal spaces and paragraph breaks.

Loading Demo...