Skip to content
On this page

Block Handles Plugin

The Block Handles Plugin provides an interactive block manipulation system for Marzipan's preview overlay. It displays visual handles on the left side of markdown blocks, allowing users to easily select, copy, and delete blocks with mouse and keyboard interactions.

Overview

The Block Handles Plugin enhances the editing experience by providing visual, interactive controls for manipulating markdown blocks. Each block type gets a unique icon, and users can select, copy, or delete blocks using mouse or keyboard shortcuts.

Why Block Handles?

Block handles make complex editing tasks intuitive by providing visual feedback and direct manipulation. Perfect for restructuring documents, copying sections, and cleaning up content.

Features

  • 🎨 Visual Handles: Unique icons for each block type (headings, paragraphs, lists, quotes, code, tables, etc.)
  • πŸ–±οΈ Hover Interaction: Handles appear when hovering over blocks
  • ✨ Selection System: Click handles or Shift+Click blocks to select them
  • πŸ“‹ Context Menu: Right-click handles for quick actions (copy, delete, select)
  • ⌨️ Keyboard Shortcuts: Ctrl/Cmd+C to copy, Delete/Backspace to delete selected blocks
  • 🎯 Visual Feedback: Highlight effects for hover and selection states
  • πŸ”” Toast Notifications: User-friendly feedback for actions
  • πŸ“‘ Event System: Custom events for block selection and deselection

Installation

The plugin is included with Marzipan by default. Simply enable it in your editor configuration:

ts
import { Marzipan } from '@pinkpixel/marzipan';

const editor = new Marzipan('#editor', {
  blockHandles: true  // Enabled by default
});

Configuration

Basic Configuration

ts
const editor = new Marzipan('#editor', {
  blockHandles: {
    enabled: true,           // Enable/disable the plugin
    showOnHover: true,       // Show handles on hover
    handleOffset: -30,       // Horizontal offset from block (px)
    handleSize: 20,          // Handle size (px)
    colors: {
      hover: 'rgba(59, 130, 246, 0.1)',      // Hover highlight color
      selected: 'rgba(59, 130, 246, 0.2)',   // Selection highlight color
      handle: 'rgba(59, 130, 246, 0.8)',     // Handle background color
    }
  }
});

Configuration Options

OptionTypeDefaultDescription
enabledbooleantrueEnable or disable the plugin
showOnHoverbooleantrueShow handles when hovering over blocks
handleOffsetnumber-30Horizontal offset of handles from blocks (in pixels)
handleSizenumber20Size of handle buttons (in pixels)
colors.hoverstring'rgba(59, 130, 246, 0.1)'Background color when hovering over blocks
colors.selectedstring'rgba(59, 130, 246, 0.2)'Background color for selected blocks
colors.handlestring'rgba(59, 130, 246, 0.8)'Background color of handle buttons

Usage

Basic Interactions

Mouse Interactions

  1. Hover: Move your mouse over any markdown block to see its handle appear on the left
  2. Click Handle: Click a handle to select the block
  3. Right-Click Handle: Right-click to open the context menu with available actions
  4. Shift+Click Block: Hold Shift and click anywhere on a block to select it

Keyboard Shortcuts

  • Escape: Deselect the currently selected block
  • Ctrl/Cmd+C: Copy the selected block to clipboard
  • Delete or Backspace: Delete the selected block

Context Menu Actions

Right-click any handle to access these actions:

  • Copy: Copy the block's content to clipboard
  • Delete: Remove the block from the document
  • Select: Select the block (same as clicking the handle)

Block Types and Icons

Each markdown block type has a unique icon:

Block TypeIconDescription
Heading⚑H1-H6 headers
ParagraphΒΆRegular text paragraphs
List Itemβ€’Bullet and numbered lists
Quote"Blockquotes
Code Fence{Code fence markers
Code Content{}Content inside code blocks
Horizontal Rule―Horizontal lines
Table Row⊞Table data rows
Table Separator═Table header separators

Programmatic API

Access the plugin instance through your Marzipan editor:

ts
const editor = new Marzipan('#editor', {
  blockHandles: true
});

const plugin = editor[0].blockHandlesPlugin;

Methods

refresh()

Rescan blocks and update handle positions.

ts
plugin.refresh();

updateAllHandlePositions()

Update positions of all handles (useful after scroll or resize).

ts
plugin.updateAllHandlePositions();

getSelectedBlock()

Get the currently selected block.

ts
const block = plugin.getSelectedBlock();
if (block) {
  console.log('Selected block:', block.type, block.lineStart, block.lineEnd);
}

getAllBlocks()

Get all blocks tracked by the plugin.

ts
const blocks = plugin.getAllBlocks();
console.log(`Found ${blocks.length} blocks`);

enable()

Enable the plugin.

ts
plugin.enable();

disable()

Disable the plugin and remove all handles.

ts
plugin.disable();

destroy()

Clean up and remove the plugin completely.

ts
plugin.destroy();

Events

Listen for block selection events:

ts
const preview = editor[0].preview;

preview.addEventListener('blockSelected', (e) => {
  console.log('Block selected:', e.detail.blockId, e.detail.block);
});

preview.addEventListener('blockDeselected', (e) => {
  console.log('Block deselected:', e.detail.blockId);
});

Block Object Structure

Each block object contains:

ts
interface BlockHandle {
  id: string;              // Unique block identifier
  type: string;            // Block type (heading, paragraph, etc.)
  lineStart: number;       // Starting line number in editor
  lineEnd: number;         // Ending line number in editor
  element: HTMLElement;    // DOM element for the block
  handleElement: HTMLElement | null;  // Handle DOM element
}

Advanced Usage

Custom Handle Styling

You can override handle styles with CSS:

css
/* Customize handle appearance */
.mz-block-handle {
  border: 2px solid white;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

/* Style specific block types */
.mz-block-handle-heading {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.mz-block-handle-quote {
  background: #10b981;
}

Conditional Enable/Disable

Enable handles only for certain conditions:

ts
const editor = new Marzipan('#editor', {
  blockHandles: {
    enabled: window.innerWidth > 768  // Only on desktop
  }
});

// Toggle based on user preference
window.addEventListener('resize', () => {
  if (window.innerWidth > 768) {
    editor[0].blockHandlesPlugin?.enable();
  } else {
    editor[0].blockHandlesPlugin?.disable();
  }
});

Integrate with Custom Actions

ts
const preview = editor[0].preview;

preview.addEventListener('blockSelected', (e) => {
  const block = e.detail.block;
  
  // Add custom toolbar for selected block
  showCustomToolbar(block);
  
  // Highlight corresponding line in editor
  highlightEditorLines(block.lineStart, block.lineEnd);
});

preview.addEventListener('blockDeselected', () => {
  hideCustomToolbar();
  clearEditorHighlights();
});

Complete Example

ts
import { Marzipan } from '@pinkpixel/marzipan';

const [editor] = new Marzipan('#editor', {
  // Enable toolbar
  toolbar: true,
  
  // Configure block handles
  blockHandles: {
    enabled: true,
    showOnHover: true,
    handleOffset: -30,
    handleSize: 24,
    colors: {
      hover: 'rgba(236, 72, 153, 0.1)',
      selected: 'rgba(236, 72, 153, 0.2)',
      handle: 'rgba(236, 72, 153, 0.9)',
    }
  },
  
  // Listen for changes
  onChange: (value, instance) => {
    console.log('Content changed');
  }
});

// Access plugin
const plugin = editor.blockHandlesPlugin;

// Listen for selection events
editor.preview.addEventListener('blockSelected', (e) => {
  console.log('Block selected:', e.detail.block.type);
});

// Programmatic control
document.getElementById('refreshBtn').addEventListener('click', () => {
  plugin.refresh();
});

Browser Compatibility

The Block Handles Plugin requires:

  • Modern browser with ES6+ support
  • navigator.clipboard API for copy functionality
  • CSS Grid support for handle positioning

Supported browsers:

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+

Troubleshooting

Handles Not Appearing

  1. Verify the plugin is enabled:

    ts
    console.log(editor[0].blockHandlesPlugin);
    
  2. Check if block metadata is present:

    ts
    const blocks = editor[0].preview.querySelectorAll('[data-block-id]');
    console.log('Blocks found:', blocks.length);
    
  3. Ensure preview has relative positioning:

    ts
    console.log(getComputedStyle(editor[0].preview).position);
    

Handles Misaligned

Call updateAllHandlePositions() after DOM changes:

ts
editor[0].blockHandlesPlugin.updateAllHandlePositions();

Performance Issues

If you notice performance issues with many blocks:

  1. Disable hover effects:

    ts
    blockHandles: { showOnHover: false }
    
  2. Throttle position updates on scroll

Performance Considerations

For documents with 100+ blocks, consider:

  • Disabling showOnHover
  • Implementing virtual scrolling
  • Using debounced position updates

Tips and Best Practices

Best Practices

  1. Enable for Desktop Only - Block handles work best on larger screens
  2. Customize Colors - Match your application's theme for consistency
  3. Listen to Events - Integrate with your app's state management
  4. Test with Long Documents - Ensure performance with real-world content

See Also

Released under the Apache 2.0 License