EditorJS GIF Plugin

September 4, 2025 Β· View on GitHub

A customizable GIF search and embed plugin for EditorJS that allows users to search and embed GIFs from any GIF provider through a secure server-side API endpoint.

Live demo

Features

  • πŸ” Search GIFs: Search for GIFs using any provider (Tenor, Giphy, etc.)
  • βš™οΈ Highly Configurable: Customize API endpoints, UI text, styling, and behavior
  • 🎨 Responsive Design: Works on desktop and mobile devices
  • πŸ–±οΈ Horizontal Scrolling: Mouse wheel support for easy browsing
  • 🎯 Drag Prevention: Prevents conflicts with EditorJS drag operations
  • πŸ“± Touch Friendly: Optimized for touch devices
  • πŸ”Œ Provider Agnostic: Works with any GIF API that returns JSON

Installation

NPM

npm install @jingjunma/editorjs-gif

Note: This is a scoped package under @jingjunma. Scoped packages help avoid naming conflicts and clearly identify the author.

CDN

<script src="https://cdn.jsdelivr.net/npm/@jingjunma/editorjs-gif@latest/src/editorjs-gif.js"></script>

Usage

Basic Setup

const editor = new EditorJS({
  holder: 'editorjs',
  tools: {
    gif: {
      class: EditorJSGifPlugin,
      config: {
        endpoint: '/api/gifs/search', // Your server-side proxy endpoint
      }
    }
  }
});

Why Server-Side Endpoint Required?

This plugin requires a server-side endpoint because:

  1. πŸ” Security: GIF provider APIs (Tenor, Giphy) require API keys that must never be exposed in client-side code
  2. πŸ›‘οΈ Rate Limiting: Server-side control prevents API abuse and quota exhaustion
  3. πŸŽ›οΈ Control: Filter content, add caching, implement custom business logic
  4. πŸ’° Cost Control: Monitor and control API usage to manage costs

The plugin sends requests to your server, which then securely communicates with the GIF provider.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Browser       β”‚    β”‚   Your Server   β”‚    β”‚  GIF Provider   β”‚
β”‚   (EditorJS)    β”‚    β”‚   (Proxy)       β”‚    β”‚  (Tenor/Giphy)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                 β”‚    β”‚                 β”‚    β”‚                 β”‚
β”‚ πŸ” User types   │───▢│ πŸ›‘οΈ Validates    │───▢│ 🎬 Returns GIFs β”‚
β”‚ "funny cats"    β”‚    β”‚ + Adds API key  β”‚    β”‚ + metadata      β”‚
β”‚                 β”‚    β”‚ + Rate limits   β”‚    β”‚                 β”‚
β”‚ πŸ–ΌοΈ Displays     │◀───│ πŸ“¦ Processes    │◀───│                 β”‚
β”‚ GIF results     β”‚    β”‚ response        β”‚    β”‚                 β”‚
β”‚                 β”‚    β”‚                 β”‚    β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   ❌ No API Key           βœ… API Key Secure       πŸ” API Key Required

πŸ”’ Security Requirements

  • 🚨 CRITICAL: Never expose API keys in client-side code
  • βœ… REQUIRED: Always use a server-side proxy for API requests
  • βœ… REQUIRED: Validate and sanitize all user inputs
  • βœ… RECOMMENDED: Implement rate limiting on your API endpoint
  • βœ… RECOMMENDED: Use HTTPS for all API communications
  • βœ… RECOMMENDED: Store API keys securely (never in code)
  • βœ… RECOMMENDED: Add request logging for monitoring

Complete Example

const editor = new EditorJS({
  holder: 'editorjs',
  tools: {
    gif: {
      class: EditorJSGifPlugin,
      config: {
        endpoint: '/api/gifs/search', // Your server-side proxy endpoint
        limit: 20,
        poweredByText: 'Powered by Tenor'
      }
    }
  }
});

TypeScript Usage

import EditorJS from '@editorjs/editorjs';
import EditorJSGifPlugin, { EditorJSGifConfig } from '@jingjunma/editorjs-gif';

const gifConfig: EditorJSGifConfig = {
  endpoint: '/api/gifs/search',
  limit: 20,
  poweredByText: 'Powered by Tenor',
  previewHeight: 200
};

const editor = new EditorJS({
  holder: 'editorjs',
  tools: {
    gif: {
      class: EditorJSGifPlugin,
      config: gifConfig
    }
  }
});

With Custom API

const editor = new EditorJS({
  holder: 'editorjs',
  tools: {
    gif: {
      class: EditorJSGifPlugin,
      config: {
        endpoint: 'https://your-custom-api.com/search',
        headers: {
          'Authorization': 'Bearer YOUR_TOKEN'
        },
        responseParser: (response) => {
          // Transform your API response to the expected format
          return response.data.map(gif => ({
            id: gif.id,
            previewUrl: gif.thumbnail_url,
            fullUrl: gif.gif_url,
            width: gif.width,
            height: gif.height,
            title: gif.title,
            alt: gif.description || gif.title
          }));
        }
      }
    }
  }
});

Configuration Options

OptionTypeDefaultDescription
endpointstringnullRequired - Your server-side API endpoint for GIF search
limitnumber15Number of GIFs to fetch per search
placeholderstring'Search for GIFs...'Search input placeholder text
buttonTextstring'Search'Search button text
removeButtonTextstring'Remove GIF'Remove button text
poweredByTextstring''Optional branding text (e.g., "Powered by Tenor")
previewHeightnumber200Height of preview GIFs in pixels
enableHorizontalScrollbooleantrueEnable mouse wheel horizontal scrolling
debounceDelaynumber300Debounce delay in milliseconds for search input
headersobject{}Additional headers for API requests
responseParserfunctionBuilt-in parserCustom function to parse API responses

API Response Format

The plugin expects your API to return GIF data in a specific format. You can either:

  1. Use the default format (compatible with Tenor API):
{
  "results": [
    {
      "id": "gif_id",
      "content_description": "Description",
      "media_formats": {
        "gif": {
          "url": "https://example.com/full.gif",
          "dims": [width, height]
        },
        "tinygif": {
          "url": "https://example.com/preview.gif"
        }
      }
    }
  ]
}
  1. Use a custom responseParser to transform your API response:
config: {
  endpoint: 'https://your-api.com/search',
  responseParser: (response) => {
    return response.gifs.map(gif => ({
      id: gif.id,
      previewUrl: gif.preview_url,
      fullUrl: gif.full_url,
      width: gif.dimensions.width,
      height: gif.dimensions.height,
      title: gif.title,
      alt: gif.description
    }));
  }
}

Required: Server-Side Implementation

You MUST implement a server-side proxy to use this plugin securely. Here are examples:

Node.js/Express with Tenor API

// server.js
app.get('/api/gifs/search', async (req, res) => {
  const { q, limit = 15 } = req.query;
  const apiKey = 'YOUR_TENOR_API_KEY'; // Store securely (env vars, config, etc.)
  
  // Validate input
  if (!q || q.trim().length === 0) {
    return res.status(400).json({ error: 'Query parameter is required' });
  }
  
  try {
    const response = await fetch(
      `https://tenor.googleapis.com/v2/search?q=${encodeURIComponent(q)}&key=${apiKey}&limit=${limit}&media_filter=gif`
    );
    
    if (!response.ok) {
      throw new Error(`Tenor API error: ${response.status}`);
    }
    
    const data = await response.json();
    res.json(data);
  } catch (error) {
    console.error('GIF search error:', error);
    res.status(500).json({ error: 'Failed to fetch GIFs' });
  }
});

PHP with Tenor API

<?php
// api/gifs/search.php
function searchGifs($query, $limit = 15) {
    $apiKey = 'YOUR_TENOR_API_KEY'; // Store securely in your preferred way
    
    // Validate input
    if (empty(trim($query))) {
        http_response_code(400);
        echo json_encode(['error' => 'Query parameter is required']);
        return;
    }
    
    $url = "https://tenor.googleapis.com/v2/search?" . http_build_query([
        'q' => $query,
        'key' => $apiKey,
        'limit' => $limit,
        'media_filter' => 'gif'
    ]);
    
    $context = stream_context_create([
        'http' => [
            'timeout' => 10,
            'user_agent' => 'YourApp/1.0'
        ]
    ]);
    
    $response = file_get_contents($url, false, $context);
    
    if ($response === false) {
        http_response_code(500);
        echo json_encode(['error' => 'Failed to fetch GIFs']);
        return;
    }
    
    return json_decode($response, true);
}

// Handle the request
$query = $_GET['q'] ?? '';
$limit = min((int)($_GET['limit'] ?? 15), 50); // Cap at 50

$result = searchGifs($query, $limit);
if ($result) {
    header('Content-Type: application/json');
    echo json_encode($result);
}
?>

Performance Features

The plugin automatically debounces search input to prevent excessive API calls. By default, it waits 300ms after the user stops typing before making the search request. You can customize this:

config: {
  endpoint: '/api/gifs/search',
  debounceDelay: 500 // Wait 500ms instead of default 300ms
}

Memory Management

The plugin includes automatic cleanup to prevent memory leaks. If you need to manually clean up (e.g., when destroying the editor programmatically), you can call:

// Assuming you have access to the plugin instance
gifPluginInstance.destroy();

This will:

  • Clear any pending search timeouts
  • Remove all event listeners
  • Clean up DOM references

Styling

The plugin includes default styles, but you can customize the appearance:

/* Customize the search input */
.editorjs-gif-search-input {
  border: 2px solid #your-color !important;
  border-radius: 8px !important;
}

/* Customize the search button */
.editorjs-gif-search-button {
  background: linear-gradient(45deg, #your-color1, #your-color2) !important;
}

/* Customize GIF previews */
.editorjs-gif-wrapper > img {
  border-radius: 12px !important;
  height: 250px !important; /* Override default height */
}

/* Customize the container */
.editorjs-gif-container {
  border: 2px dashed #your-color !important;
  background: #your-bg-color !important;
}

Browser Support

  • Chrome 60+
  • Firefox 55+
  • Safari 12+
  • Edge 79+

Development

This plugin follows the simple, single-file approach common to many EditorJS plugins. No build process is required - the plugin works directly as a standalone JavaScript file.

Setup

git clone https://github.com/jingjunma/editorjs-gif.git
cd editorjs-gif
npm install

Testing

Set up a simple HTML file to test the plugin (after implementing your server-side proxy):

<!DOCTYPE html>
<html>
<head>
    <title>EditorJS GIF Plugin Test</title>
    <script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
    <script src="./src/editorjs-gif.js"></script>
</head>
<body>
    <div id="editorjs"></div>
    <script>
        const editor = new EditorJS({
            holder: 'editorjs',
            tools: {
                gif: {
                    class: EditorJSGifPlugin,
                    config: {
                        endpoint: '/api/gifs/search' // Your server-side endpoint
                    }
                }
            }
        });
    </script>
</body>
</html>

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT License - see the LICENSE file for details.

Acknowledgments

  • Built for the amazing EditorJS community
  • Inspired by the need for better GIF integration in modern editors
  • Thanks to all contributors and testers
  • Created by Jing Jun Ma

Changelog

1.1.0 (Upcoming)

  • Added debounced search to prevent excessive API calls
  • Added destroy() method for proper cleanup and memory management
  • Improved input validation (minimum 2 characters, max 100 characters)
  • Enhanced event listener management to prevent memory leaks
  • Added TypeScript definitions for debounceDelay configuration

1.0.0

  • Initial release
  • Configurable API endpoints
  • Tenor API support out-of-the-box
  • Horizontal mouse wheel scrolling
  • Responsive design
  • Custom response parser support