Skip to content
On this page

Image Plugins

Marzipan provides two powerful image plugins for different image insertion workflows, from simple URL input to advanced management with drag-drop and paste support.

Overview

PluginBest ForFeatures
imagePickerPluginQuick URL insertionSimple URL input, instant insert
imageManagerPluginFull image managementUpload, recents, drag-drop, paste, thumbnails

Both plugins insert markdown image syntax:

markdown
![Alt text](https://example.com/image.png)

Image Picker Plugin

Quick and simple image URL insertion.

Installation

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

new Marzipan('#editor', {
  toolbar: true,
  plugins: [imagePickerPlugin()]
});

Options

ts
interface ImagePickerOptions {
  label?: string;      // Toolbar button label (default: 🖼️)
  title?: string;      // Tooltip text (default: 'Insert image')
  placeholder?: string; // Default URL shown in the prompt
  promptMessage?: string; // Custom prompt message
}

Usage Example

ts
import { imagePickerPlugin } from '@pinkpixel/marzipan/plugins/imagePickerPlugin';

new Marzipan('#editor', {
  plugins: [
    imagePickerPlugin({
      label: '📷',
      title: 'Add an image',
      placeholder: 'Enter image URL...'
    })
  ]
});

Features

  • ✅ Simple URL input dialog
  • ✅ Instant markdown insertion
  • ✅ Lightweight and fast
  • ✅ No storage or complexity
  • ✅ Perfect for basic needs
  • ✅ Customise the prompt message and default URL

How It Works

  1. Click the image picker button
  2. Enter image URL in the prompt
  3. Image markdown is inserted at cursor
  4. Preview updates automatically

User Experience

Click button → Enter URL: "https://example.com/photo.jpg"
                           
Result inserted:
![photo.jpg](https://example.com/photo.jpg)

Image Manager Plugin

Full-featured image management with recents, upload, and more.

Installation

ts
import { imageManagerPlugin } from '@pinkpixel/marzipan/plugins/imageManagerPlugin';

new Marzipan('#editor', {
  plugins: [imageManagerPlugin()]
});

Options

ts
interface ImageManagerOptions {
  label?: string;              // Toolbar button (default: 🗂️)
  title?: string;              // Tooltip (default: 'Images')
  maxRecent?: number;          // Max recent images (default: 24)
  persistThresholdBytes?: number; // localStorage size limit (default: 1MB)
  uploader?: (file: File) => Promise<string>; // Custom upload handler
  onInsert?: (url: string) => string;         // Transform URL before insert
}

Usage Examples

Basic Usage:

ts
import { imageManagerPlugin } from '@pinkpixel/marzipan/plugins/imageManagerPlugin';

new Marzipan('#editor', {
  plugins: [imageManagerPlugin()]
});

With Custom Upload:

ts
import { imageManagerPlugin } from '@pinkpixel/marzipan/plugins/imageManagerPlugin';

new Marzipan('#editor', {
  plugins: [
    imageManagerPlugin({
      maxRecent: 12,
      
      // Upload to your server
      uploader: async (file) => {
        const formData = new FormData();
        formData.append('image', file);
        
        const response = await fetch('/api/upload', {
          method: 'POST',
          body: formData
        });
        
        const data = await response.json();
        return data.url; // Return the uploaded image URL
      },
      
      // Transform URLs (e.g., add CDN prefix)
      onInsert: (url) => {
        if (url.startsWith('http')) return url;
        return `https://cdn.example.com${url}`;
      }
    })
  ]
});

Cloudflare R2 Upload Example:

ts
imageManagerPlugin({
  uploader: async (file) => {
    // Generate presigned URL for R2
    const presignedResponse = await fetch('/api/upload-url', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        filename: file.name,
        contentType: file.type
      })
    });
    
    const { uploadUrl, publicUrl } = await presignedResponse.json();
    
    // Upload to R2
    await fetch(uploadUrl, {
      method: 'PUT',
      body: file,
      headers: { 'Content-Type': file.type }
    });
    
    return publicUrl;
  }
})

Features

  • Recent Images - Thumbnail grid of recent uploads
  • Drag & Drop - Drop files onto editor or panel
  • Paste Support - Paste images from clipboard
  • URL Import - Add images from URLs
  • Local Upload - Upload files from device
  • localStorage Persistence - Small images saved locally
  • Custom Upload - Integrate with your backend
  • Thumbnail Preview - Visual selection
  • Remove/Clear - Manage recent images

How It Works

Adding Images:

  1. Click the images button
  2. Choose method:
    • Add URL: Enter image URL
    • Upload: Select file from device
    • Drag-drop: Drop file on drop zone or editor
    • Paste: Copy image and paste in editor

Using Recents:

  1. Open images panel
  2. Click thumbnail to insert
  3. Right-click × to remove from recents

Storage Behavior

Small Files (< 1MB by default):

  • Converted to data URLs
  • Stored in localStorage
  • Available across sessions
  • Instant loading

Large Files:

  • Created as blob URLs
  • Session-only (cleared on page refresh)
  • No localStorage usage

With Custom Uploader:

  • All files uploaded to your server
  • Remote URLs stored in recents
  • Persistent across sessions

Styling

The plugin exports imageManagerStyles for custom styling:

ts
import { 
  imageManagerPlugin, 
  imageManagerStyles 
} from '@pinkpixel/marzipan/plugins/imageManagerPlugin';

// Inject styles
const styleElement = document.createElement('style');
styleElement.textContent = imageManagerStyles;
document.head.appendChild(styleElement);

new Marzipan('#editor', {
  plugins: [imageManagerPlugin()]
});

Custom CSS Variables

css
/* Image Manager Panel */
--mz-ip-bg: #0f1216;      /* Input background */
--mz-ip-fg: #e7e7e7;      /* Input text */
--mz-ip-bd: #2b2f36;      /* Input border */

--mz-btn-bg: #1a1e24;     /* Button background */
--mz-btn-fg: #e7e7e7;     /* Button text */
--mz-btn2-bg: #1b2027;    /* Card button background */
--mz-btn2-fg: #e7e7e7;    /* Card button text */

--mz-drop-bg: #0e1116;    /* Drop zone background */
--mz-drop-on: #172033;    /* Drop zone hover */
--mz-drop-bd: #3b4350;    /* Drop zone border */

--mz-card-bg: #12151a;    /* Thumbnail card background */
--mz-card-bd: #2b2f36;    /* Thumbnail card border */

Complete Example

ts
import { Marzipan } from '@pinkpixel/marzipan';
import { 
  imageManagerPlugin,
  imageManagerStyles 
} from '@pinkpixel/marzipan/plugins';

// Inject styles
const style = document.createElement('style');
style.textContent = imageManagerStyles;
document.head.appendChild(style);

const [editor] = new Marzipan('#editor', {
  toolbar: true,
  theme: 'cave',
  
  plugins: [
    imageManagerPlugin({
      maxRecent: 16,
      persistThresholdBytes: 500 * 1024, // 500KB limit
      
      // Custom upload to your API
      uploader: async (file) => {
        const formData = new FormData();
        formData.append('file', file);
        
        const res = await fetch('/api/images/upload', {
          method: 'POST',
          body: formData,
          headers: {
            'Authorization': `Bearer ${localStorage.getItem('token')}`
          }
        });
        
        if (!res.ok) throw new Error('Upload failed');
        const data = await res.json();
        return data.imageUrl;
      },
      
      // Add alt text transformation
      onInsert: (url) => {
        // Could add width/height parameters
        return url;
      }
    })
  ],
  
  value: `# My Document

Here are some images:

![Example](https://example.com/photo.jpg)
`
});

Image Best Practices

Optimization

  1. Compress images before uploading
  2. Use appropriate formats (WebP when possible)
  3. Set size limits to prevent huge uploads
  4. Use lazy loading for many images
  5. Consider CDN for production apps

Alt Text

Always include descriptive alt text for accessibility:

markdown
![A sunset over the ocean](https://example.com/sunset.jpg)

Not just:

markdown
![image](https://example.com/sunset.jpg)

Drag & Drop

The image manager supports drag & drop in two areas:

Drop Zone in Panel

Drop images onto the highlighted drop zone for quick upload.

Drop on Editor

Drop images anywhere on the editor container for instant insertion.

ts
// Both areas support multiple files at once
// Files are processed sequentially

Paste Support

Copy images from:

  • Screenshots (⌘+Shift+4 on Mac, Win+Shift+S on Windows)
  • Other applications
  • Web browsers

Then paste directly into the editor:

  1. Focus the editor textarea
  2. Paste (⌘+V / Ctrl+V)
  3. Image is automatically processed and inserted

Upload API Integration

Example backend endpoints:

Simple Upload (Express.js)

js
// server.js
const express = require('express');
const multer = require('multer');
const path = require('path');

const app = express();
const upload = multer({ 
  dest: 'uploads/',
  limits: { fileSize: 5 * 1024 * 1024 } // 5MB limit
});

app.post('/api/upload', upload.single('image'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file uploaded' });
  }
  
  const url = `/uploads/${req.file.filename}`;
  res.json({ url });
});

app.use('/uploads', express.static('uploads'));
app.listen(3000);

S3/R2 Upload (Node.js)

js
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

const s3 = new S3Client({
  region: 'auto',
  endpoint: process.env.R2_ENDPOINT,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY,
    secretAccessKey: process.env.R2_SECRET_KEY
  }
});

app.post('/api/upload-url', async (req, res) => {
  const { filename, contentType } = req.body;
  const key = `images/${Date.now()}-${filename}`;
  
  const command = new PutObjectCommand({
    Bucket: process.env.R2_BUCKET,
    Key: key,
    ContentType: contentType
  });
  
  const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
  const publicUrl = `${process.env.R2_PUBLIC_URL}/${key}`;
  
  res.json({ uploadUrl, publicUrl });
});

Security Considerations

When implementing image uploads:

File Validation

ts
imageManagerPlugin({
  uploader: async (file) => {
    // Validate file type
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    if (!allowedTypes.includes(file.type)) {
      throw new Error('Invalid file type');
    }
    
    // Validate file size
    const maxSize = 5 * 1024 * 1024; // 5MB
    if (file.size > maxSize) {
      throw new Error('File too large');
    }
    
    // Upload logic...
  }
})

Server-Side Validation

js
app.post('/api/upload', upload.single('image'), (req, res) => {
  const file = req.file;
  
  // Check MIME type
  const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
  if (!allowedMimes.includes(file.mimetype)) {
    return res.status(400).json({ error: 'Invalid file type' });
  }
  
  // Check file size
  if (file.size > 5 * 1024 * 1024) {
    return res.status(400).json({ error: 'File too large' });
  }
  
  // Process upload...
});

Content Security

  1. Sanitize filenames to prevent path traversal
  2. Generate unique IDs for stored files
  3. Scan for malware if accepting user uploads
  4. Use signed URLs for temporary access
  5. Implement rate limiting to prevent abuse

Browser Compatibility

Image plugins work in:

  • Chrome/Edge 60+
  • Firefox 55+
  • Safari 11+

Clipboard API (paste support):

  • Chrome/Edge 76+
  • Firefox 87+
  • Safari 13.1+

File System Access (drag-drop):

  • All modern browsers

Troubleshooting

Images Not Persisting

Check localStorage size:

js
// Current usage
const used = new Blob(Object.values(localStorage)).size;
console.log(`localStorage: ${(used / 1024).toFixed(2)} KB`);

Reduce persistThresholdBytes if hitting limits.

Upload Fails

  1. Check CORS headers on your server
  2. Verify authentication tokens
  3. Check file size limits
  4. Validate file types match server expectations

Paste Not Working

  1. Ensure editor textarea has focus
  2. Check browser clipboard permissions
  3. Verify secure context (HTTPS or localhost)

See Also

Released under the Apache 2.0 License