Features

    Features

    A complete overview of everything Scribe Editor supports out of the box — from basic formatting to advanced features like inline widgets, variables, and iframe editing.

    Feature Matrix

    Complete list of supported formatting features

    Inline

    Bold
    .bold()
    Italic
    .italic()
    Underline
    .underline()
    Strikethrough
    .strike()
    Inline Code
    .code()
    Subscript
    .subscript()
    Superscript
    .superscript()

    Block

    Headings (H1-H6)
    .heading(1-6)
    Paragraph
    .paragraph()
    Blockquote
    .blockquote()
    Code Block
    .codeBlock()
    Horizontal Rule
    .insertHR()

    Lists

    Ordered List
    .orderedList()
    Unordered List
    .unorderedList()
    Nested Lists
    Tab/Shift+Tab

    Alignment

    Left
    .alignLeft()
    Center
    .alignCenter()
    Right
    .alignRight()
    Justify
    .alignJustify()
    Indent
    .indent()
    Outdent
    .outdent()

    Links & Media

    Links
    .link(url)
    Unlink
    .unlink()
    Images
    .insertHTML()
    Embeds
    Plugin

    Images & Embeds

    Inserting and handling media content

    Images are inserted via insertHTML(). For file uploads, handle the upload separately and insert the resulting URL.
    // Image insertion
    const editor = Scribe.init('#editor');
    
    // Insert image HTML
    editor.insertHTML('<img src="image.jpg" alt="Description" />');
    
    // With full attributes
    editor.insertHTML(`
      <figure>
        <img src="photo.jpg" alt="A beautiful sunset" width="800" height="600" />
        <figcaption>Photo by John Doe</figcaption>
      </figure>
    `);
    
    // Handle image upload
    async function handleImageUpload(file: File) {
      const formData = new FormData();
      formData.append('image', file);
      
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
      });
      
      const { url } = await response.json();
      editor.insertHTML(`<img src="${url}" alt="" />`);
    }
    
    // Drag and drop
    const content = editor.getContentElement();
    content.addEventListener('drop', async (e) => {
      e.preventDefault();
      const files = Array.from(e.dataTransfer?.files || []);
      
      for (const file of files) {
        if (file.type.startsWith('image/')) {
          await handleImageUpload(file);
        }
      }
    });

    Variables / Tokens

    Insertable placeholders for mail merge and templates

    // Variable/Token insertion (mail merge, templates)
    const editor = Scribe.init('#editor');
    
    // Insert a non-editable variable token
    function insertVariable(name: string, value: string) {
      const token = `<span 
        class="variable-token" 
        contenteditable="false" 
        data-variable="${name}"
      >${value}</span>`;
      
      editor.insertHTML(token);
      editor.focus();
    }
    
    // Usage
    insertVariable('first_name', '{{first_name}}');
    insertVariable('company', '{{company}}');
    
    // Style in CSS
    // .variable-token {
    //   background: #e3f2fd;
    //   border-radius: 3px;
    //   padding: 0 4px;
    //   color: #1565c0;
    //   cursor: default;
    // }
    
    // Extract variables from content
    function extractVariables(html: string): string[] {
      const matches = html.match(/data-variable="([^"]+)"/g) || [];
      return matches.map(m => m.replace(/data-variable="|"/g, ''));
    }

    Inline Widgets

    Custom non-editable elements (mentions, hashtags, etc.)

    Widget Structure

    <span class="widget widget-mention" 
          contenteditable="false" 
          data-type="mention" 
          data-payload='{"id":"123","name":"John"}'>
      <span class="mention">@John</span>
    </span>
    // Inline widgets (mentions, hashtags, custom blocks)
    class InlineWidget {
      constructor(private editor: EditorInstance) {}
      
      insert(type: string, data: Record<string, unknown>) {
        const widget = document.createElement('span');
        widget.className = `widget widget-${type}`;
        widget.contentEditable = 'false';
        widget.dataset.type = type;
        widget.dataset.payload = JSON.stringify(data);
        
        // Render based on type
        switch (type) {
          case 'mention':
            widget.innerHTML = `<span class="mention">@${data.name}</span>`;
            break;
          case 'hashtag':
            widget.innerHTML = `<span class="hashtag">#${data.tag}</span>`;
            break;
          case 'emoji':
            widget.innerHTML = data.emoji as string;
            break;
        }
        
        this.editor.insertHTML(widget.outerHTML);
      }
      
      // Parse widgets from HTML
      static parse(html: string): Array<{ type: string; data: unknown }> {
        const doc = new DOMParser().parseFromString(html, 'text/html');
        const widgets = doc.querySelectorAll('.widget');
        
        return Array.from(widgets).map(w => ({
          type: w.dataset.type!,
          data: JSON.parse(w.dataset.payload || '{}')
        }));
      }
    }
    
    // Usage
    const widgets = new InlineWidget(editor);
    widgets.insert('mention', { id: '123', name: 'John' });
    widgets.insert('hashtag', { tag: 'important' });

    Toolbar Positioning

    Floating toolbar placement logic

    Positioning Algorithm

    ┌─────────────────────────────────────────────────┐
    │  Viewport                                       │
    │                                                 │
    │  ┌─ Toolbar ─┐  ← Centered on selection         │
    │  │  B I U L  │                                  │
    │  └───────────┘                                  │
    │       │                                         │
    │       ▼  8px gap                                │
    │  ┌─────────────────────────────────────────┐   │
    │  │  Selected text in the editor content    │   │
    │  └─────────────────────────────────────────┘   │
    │                                                 │
    │  If selection near top:                         │
    │                                                 │
    │  ┌─────────────────────────────────────────┐   │
    │  │  Selected text near top of viewport     │   │
    │  └─────────────────────────────────────────┘   │
    │       ▼  8px gap                                │
    │  ┌─ Toolbar ─┐  ← Flips below selection         │
    │  │  B I U L  │                                  │
    │  └───────────┘                                  │
    └─────────────────────────────────────────────────┘
    // Toolbar positioning logic
    function positionFloatingToolbar(
      toolbar: HTMLElement,
      selectionRect: DOMRect
    ) {
      const toolbarHeight = toolbar.offsetHeight;
      const toolbarWidth = toolbar.offsetWidth;
      const padding = 8;
      
      // Calculate position above selection
      let x = selectionRect.left + selectionRect.width / 2;
      let y = selectionRect.top - toolbarHeight - padding;
      
      // Flip below if too close to top
      if (y < padding) {
        y = selectionRect.bottom + padding;
      }
      
      // Keep within viewport horizontally
      const maxX = window.innerWidth - toolbarWidth / 2 - padding;
      const minX = toolbarWidth / 2 + padding;
      x = Math.max(minX, Math.min(maxX, x));
      
      // Apply position
      toolbar.style.left = `${x}px`;
      toolbar.style.top = `${y}px`;
      toolbar.style.transform = 'translateX(-50%)';
    }
    
    // Usage with selection events
    editor.on('selectionChange', (selection) => {
      if (selection && !selection.collapsed && selection.rect) {
        positionFloatingToolbar(toolbar, selection.rect);
        toolbar.classList.add('visible');
      } else {
        toolbar.classList.remove('visible');
      }
    });

    Mobile Support

    Touch devices and virtual keyboard handling

    Touch Targets

    Minimum 44×44px buttons for reliable touch interaction

    Virtual Keyboard

    Auto-scroll selection into view when keyboard opens

    iOS Quirks

    Delayed selection detection for reliable toolbar display
    // Mobile support considerations
    const editor = Scribe.init('#editor', {
      // Consider touch events
    });
    
    // 1. Touch-friendly toolbar sizing
    // CSS:
    // .toolbar button { min-width: 44px; min-height: 44px; }
    
    // 2. Virtual keyboard handling
    const content = editor.getContentElement();
    
    // Scroll selection into view when keyboard appears
    content.addEventListener('focus', () => {
      setTimeout(() => {
        const selection = editor.getSelection();
        if (selection?.rect) {
          // Scroll into viewport center
          window.scrollTo({
            top: selection.rect.top - window.innerHeight / 3,
            behavior: 'smooth'
          });
        }
      }, 300); // Wait for keyboard
    });
    
    // 3. Handle iOS quirks
    // iOS doesn't fire selectionchange reliably
    let lastTouch: Touch | null = null;
    
    content.addEventListener('touchend', (e) => {
      lastTouch = e.changedTouches[0];
      
      // Delayed selection check
      setTimeout(() => {
        const selection = editor.getSelection();
        if (selection && !selection.collapsed) {
          showToolbar(selection);
        }
      }, 10);
    });
    
    // 4. Responsive toolbar
    function getToolbarMode(): 'full' | 'compact' {
      return window.innerWidth < 768 ? 'compact' : 'full';
    }
    
    // Compact mode shows fewer buttons with overflow menu

    Paste Sanitization

    Automatic cleanup of pasted content

    Preserved

    Bold, italic, links, lists, headings, paragraphs, semantic markup

    Removed

    Scripts, iframes, event handlers, Word styles, external fonts, tracking pixels
    // Paste sanitization
    const editor = Scribe.init('#editor', {
      sanitize: {
        allowedTags: ['p', 'strong', 'em', 'a', 'ul', 'ol', 'li'],
        allowedSchemes: ['https']
      }
    });
    
    // Scribe automatically sanitizes pasted content
    // Additional custom handling:
    
    const content = editor.getContentElement();
    
    content.addEventListener('paste', (e) => {
      // Access raw clipboard data
      const html = e.clipboardData?.getData('text/html');
      const text = e.clipboardData?.getData('text/plain');
      
      // Log what was pasted (for debugging)
      console.log('Pasted HTML:', html);
      console.log('Pasted text:', text);
      
      // Note: Scribe handles sanitization automatically
      // This event fires BEFORE Scribe processes the paste
    });
    
    // Word/Google Docs cleanup is automatic:
    // - Removes mso-* styles (Microsoft Word)
    // - Strips Google Docs wrapper classes
    // - Normalizes nested lists
    // - Preserves semantic formatting (bold, italic, links)
    // - Removes font-family, font-size unless allowed

    Keyboard Shortcuts

    Built-in shortcuts for power users

    Formatting

    BoldCtrl/Cmd + B
    ItalicCtrl/Cmd + I
    UnderlineCtrl/Cmd + U
    LinkCtrl/Cmd + K
    Clear FormatCtrl/Cmd + \

    History & Navigation

    UndoCtrl/Cmd + Z
    RedoCtrl/Cmd + Y
    Redo (Alt)Ctrl/Cmd + Shift + Z
    IndentTab
    OutdentShift + Tab