Building a Paint Format Plugin: A Case Study in Froala’s Clean Plugin Architecture

Building a Paint Format Plugin: A Case Study in Froala’s Clean Plugin Architecture

Publish Date: Mar 13
0 0

If you’ve ever used Microsoft Word or Google Docs, you’re familiar with the Format Painter tool — that magical brush icon that lets you copy formatting from one text block and apply it to another with a single click. It’s one of those features that seems simple on the surface but dramatically improves editing efficiency.

Today, we’re going to build this exact functionality for Froala Editor, and in doing so, reveal the elegant architecture that makes Froala one of the most extensible WYSIWYG editors available.

Why This Matters: The Power of Custom Plugins

Froala Editor ships with an impressive array of features out of the box, but the real magic lies in its modular plugin architecture. Unlike monolithic editors that force you to work within rigid constraints, Froala treats extensibility as a first-class citizen.

This architectural decision means you’re never locked into a predetermined feature set. Need a custom workflow? Build a plugin.

The Paint Format feature we’re building today demonstrates several critical aspects of Froala’s architecture:

  • Clean API design that separates concerns between UI, state management, and content manipulation

  • Event-driven architecture that hooks into the editor’s lifecycle

  • Format API that abstracts away DOM complexity

  • Undo/redo integration that maintains editing history integrity

  • Toolbar integration that provides visual feedback and state management

Let’s dive into the code and see how these pieces fit together.

The Complete Paint Format Plugin: Architecture Overview

Our Paint Format plugin follows Froala’s standard plugin pattern, which consists of three main components:

  1. Icon Definition — Registers the visual representation in the toolbar

  2. Command Registration — Connects the toolbar button to plugin logic

  3. Plugin Implementation — Contains the core functionality and state management

Here’s the complete implementation with detailed explanations:

// ============================================================
// FORMAT PAINTER (Paint Format) PLUGIN FOR FROALA V5
// ============================================================

(function (FroalaEditor) {

 // ------------------------------------------------------------
 // 1️⃣ Define Toolbar Icon
 // ------------------------------------------------------------
FroalaEditor.DefineIcon("paintFormat", {

 template: "svgMultiplePath",

 PATHS: ``,

 VIEWBOX: "0 0 1024 1024",

});

 // ------------------------------------------------------------
 // 2️⃣ Register Toolbar Command
 // ------------------------------------------------------------
 FroalaEditor.RegisterCommand('paintFormat', {
   title: 'Paint Format',
   focus: false,
   undo: true,
   refreshAfterCallback: false,

   callback: function () {
     this.paintFormat.toggle();
   },

   refresh: function ($btn) {
     if (this.paintFormat.isActive()) {
       $btn.addClass('fr-active');
     } else {
       $btn.removeClass('fr-active');
     }
   }
 });

 // ------------------------------------------------------------
 // 3️⃣ Plugin Definition
 // ------------------------------------------------------------
 FroalaEditor.PLUGINS.paintFormat = function (editor) {

   let active = false;
   let storedFormats = null;

   // ==========================================================
   // CAPTURE FORMATTING FROM CURRENT SELECTION
   // ==========================================================
   function captureFormats() {
     const element = editor.selection.element();
     if (!element) return;

     const computed = window.getComputedStyle(element);

     storedFormats = {
       bold: editor.format.is('strong'),
       italic: editor.format.is('em'),
       underline: editor.format.is('u'),
       strike: editor.format.is('strike'),

       fontSize: computed.fontSize,
       color: computed.color,
       backgroundColor: computed.backgroundColor,
       fontFamily: computed.fontFamily,

       textAlign: computed.textAlign
     };
   }

   // ==========================================================
   // APPLY STORED FORMATTING TO NEW SELECTION
   // ==========================================================
   function applyFormats() {
     if (!storedFormats) return;

     editor.undo.saveStep();

     if (storedFormats.bold) editor.format.apply('strong');
     if (storedFormats.italic) editor.format.apply('em');
     if (storedFormats.underline) editor.format.apply('u');
     if (storedFormats.strike) editor.format.apply('strike');

     if (storedFormats.fontSize) {
       editor.format.applyStyle('font-size', storedFormats.fontSize);
     }

     if (storedFormats.color) {
       editor.format.applyStyle('color', storedFormats.color);
     }

     if (
       storedFormats.backgroundColor &&
       storedFormats.backgroundColor !== 'rgba(0, 0, 0, 0)'
     ) {
       editor.format.applyStyle(
         'background-color',
         storedFormats.backgroundColor
       );
     }

     if (storedFormats.fontFamily) {
       editor.format.applyStyle('font-family', storedFormats.fontFamily);
     }

     if (storedFormats.textAlign) {
       const blocks = editor.selection.blocks();
       if (blocks && blocks.length) {
         blocks.forEach(block => {
           block.style.textAlign = storedFormats.textAlign;
         });
       }
     }

     editor.undo.saveStep();
   }

   // ==========================================================
   // TOGGLE PAINTER MODE
   // ==========================================================
   function toggle() {
     if (!active) {
       captureFormats();
       if (!storedFormats) return;
       active = true;
     } else {
       active = false;
     }
     editor.toolbar.refresh();
   }

   function isActive() {
     return active;
   }

   // ==========================================================
   // EVENT BINDINGS
   // ==========================================================
   function bindEvents() {
     editor.events.on('mouseup', function () {
       if (!active) return;

       setTimeout(function () {
         if (editor.selection.isCollapsed()) return;
         applyFormats();
       }, 0);
     });

     editor.events.on('keydown', function (e) {
       if (e.key === 'Escape' && active) {
         active = false;
         editor.toolbar.refresh();
       }
     });
   }

   function _init() {
     bindEvents();
   }

   return {
     _init: _init,
     toggle: toggle,
     isActive: isActive
   };
 };

})(FroalaEditor);​
Enter fullscreen mode Exit fullscreen mode

Breaking Down the Architecture

Want to see this in action? Try the Paint Format plugin live on JSFiddle — click the paint brush icon, select some text with different formatting, then paint that style onto other content. It’s the fastest way to understand how all these pieces work together in real time.

Icon Definition: Visual Identity

The first step in creating any toolbar-based plugin is defining its visual representation. Froala’s DefineIcon method provides a flexible system that supports both FontAwesome icons and custom SVG graphics:

FroalaEditor.DefineIcon("paintFormat", {
 template: "svgMultiplePath",
 PATHS: ``,
 VIEWBOX: "0 0 1024 1024",
});
Enter fullscreen mode Exit fullscreen mode

The template: "svgMultiplePath" property instructs Froala to render this as an inline SVG graphic, giving you complete control over the icon’s visual appearance. The PATHS property contains the full SVG path data that defines the shape of the paint bucket icon—a detailed vector graphic that scales perfectly at any size. The VIEWBOX: "0 0 1024 1024" establishes the coordinate system for the SVG, defining a 1024×1024 viewbox that ensures consistent rendering. By using inline SVG instead of external icon libraries or font icons, you eliminate dependencies on third-party icon sets and gain complete flexibility over styling, animation, and appearance. The icon identifier ('paintFormat') becomes the namespace for your entire plugin—a convention that keeps the codebase organized and prevents naming collisions.

Command Registration: Bridging UI and Logic

The RegisterCommand method is where UI meets functionality. This is Froala’s way of creating a clean separation between presentation and business logic:

FroalaEditor.RegisterCommand('paintFormat', {
 title: 'Paint Format',
 focus: false,
 undo: true,
 refreshAfterCallback: false,

 callback: function () {
   this.paintFormat.toggle();
 },

 refresh: function ($btn) {
   if (this.paintFormat.isActive()) {
     $btn.addClass('fr-active');
   } else {
     $btn.removeClass('fr-active');
   }
 }
});
Enter fullscreen mode Exit fullscreen mode

The callback function executes when the user clicks the toolbar button. Notice how it delegates to this.paintFormat.toggle()—this is Froala’s plugin system in action. The this context refers to the editor instance, and paintFormat is automatically available because we’re defining it in the PLUGINS namespace.

The refresh function is called whenever the toolbar updates, allowing us to maintain visual state. By adding or removing the fr-active class, we create that satisfying toggle effect where the button stays highlighted while Paint Format mode is active.

Plugin Core: State Management and Format Manipulation

The heart of our plugin lives in the PLUGINS.paintFormat function. This follows the revealing module pattern, exposing only the methods that need to be public while keeping internal state private:

FroalaEditor.PLUGINS.paintFormat = function (editor) {
 let active = false;
 let storedFormats = null;

 // ... implementation

 return {
   _init: _init,
   toggle: toggle,
   isActive: isActive
 };
};
Enter fullscreen mode Exit fullscreen mode

The editor parameter gives us access to Froala’s entire API surface. The two state variables—active and storedFormats—maintain the plugin’s operational state across user interactions.

Capturing Formats: Reading the DOM Intelligently

The captureFormats function demonstrates Froala’s hybrid approach to content manipulation—combining high-level API calls with direct browser APIs when appropriate:

function captureFormats() {
 const element = editor.selection.element();
 if (!element) return;

 const computed = window.getComputedStyle(element);

 storedFormats = {
   bold: editor.format.is('strong'),
   italic: editor.format.is('em'),
   underline: editor.format.is('u'),
   strike: editor.format.is('strike'),

   fontSize: computed.fontSize,
   color: computed.color,
   backgroundColor: computed.backgroundColor,
   fontFamily: computed.fontFamily,

   textAlign: computed.textAlign
 };
}
Enter fullscreen mode Exit fullscreen mode

Notice the strategic use of two different APIs:

  1. Froala’s Format API (editor.format.is()) for semantic HTML elements like and . This approach respects Froala’s internal representation and ensures compatibility with other plugins.

  2. Browser’s Computed Style API (window.getComputedStyle()) for CSS properties like font size and color. This captures the actual rendered appearance, regardless of how it was applied (inline styles, CSS classes, or inherited styles).

This hybrid approach is crucial. If we only used DOM inspection, we’d miss semantic formatting. If we only used Froala’s API, we’d miss CSS-based styling. By combining both, we capture the complete formatting picture.

Applying Formats: Writing Changes Safely

The applyFormats function is where we modify the document. This is also where Froala’s architecture really shines:

function applyFormats() {
 if (!storedFormats) return;

 editor.undo.saveStep();

 if (storedFormats.bold) editor.format.apply('strong');
 if (storedFormats.italic) editor.format.apply('em');
 if (storedFormats.underline) editor.format.apply('u');
 if (storedFormats.strike) editor.format.apply('strike');

 if (storedFormats.fontSize) {
   editor.format.applyStyle('font-size', storedFormats.fontSize);
 }

 // ... more formatting applications

 editor.undo.saveStep();
}
Enter fullscreen mode Exit fullscreen mode

The editor.undo.saveStep() calls bookend our changes, creating a single undo point for the entire format application. This is critical for user experience—users expect one undo to reverse the entire Paint Format operation, not each individual style change.

The editor.format.apply() and editor.format.applyStyle() methods handle all the complexity of:

  • Splitting text nodes at selection boundaries

  • Wrapping content in appropriate HTML elements

  • Merging adjacent identical formatting

  • Maintaining proper nesting hierarchy

  • Preserving the selection after changes

If you tried to implement this with raw DOM manipulation, you’d need hundreds of lines of code and countless edge case handlers. Froala’s Format API abstracts all of this away.

Event Handling: Responding to User Actions

The bindEvents function wires up our plugin to the editor’s event system:

function bindEvents() {
 editor.events.on('mouseup', function () {
   if (!active) return;

   setTimeout(function () {
     if (editor.selection.isCollapsed()) return;
     applyFormats();
   }, 0);
 });

 editor.events.on('keydown', function (e) {
   if (e.key === 'Escape' && active) {
     active = false;
     editor.toolbar.refresh();
   }
 });
}
Enter fullscreen mode Exit fullscreen mode

The mouseup event handler is where the magic happens. When Paint Format mode is active and the user releases the mouse button (completing a selection), we apply the stored formatting. The setTimeout with zero delay is a common pattern that ensures the browser has updated the selection before we read it.

The keydown handler provides an escape hatch (literally)—pressing ESC deactivates Paint Format mode, giving users a quick way to cancel the operation without clicking the toolbar button again.

Integration: Adding the Plugin to Your Editor

Once you’ve created your plugin file (let’s call it froala-paint-format.js), integration is straightforward:

<!-- Include Froala core -->
<script src="froala_editor.min.js"></script>

<!-- Include your custom plugin -->
<script src="froala-paint-format.js"></script>

<script>
  new FroalaEditor('#editor', {
    toolbarButtons: [
      'bold', 'italic', 'underline',
      '|',
      'paintFormat',  // Your custom button
      '|',
      'formatOL', 'formatUL'
    ]
  });
</script>
Enter fullscreen mode Exit fullscreen mode

That’s it. No complex configuration, no build steps, no framework-specific adapters. The plugin automatically registers itself when the script loads, and you simply reference it by name in your toolbar configuration.

Why This Architecture Matters

The Paint Format plugin we’ve built demonstrates several architectural principles that make Froala exceptional for enterprise development:

1. Separation of Concerns: UI definition, command logic, and core functionality are cleanly separated, making the code maintainable and testable.

2. API-First Design: Instead of manipulating the DOM directly, we use Froala’s high-level APIs. This ensures our plugin remains compatible across editor versions and doesn’t conflict with other plugins.

3. Event-Driven Architecture: By hooking into the editor’s event system rather than polling or using timers, our plugin is efficient and responsive.

4. State Management: The plugin maintains its own state (active, storedFormats) without polluting the global scope or the editor’s internal state.

5. User Experience Focus: Features like the toggle button state, ESC key handling, and undo integration show attention to the details that make software feel polished.

Extending Further: Ideas for Enhancement

The beauty of Froala’s plugin system is that it’s infinitely extensible. Here are some ways you could enhance this Paint Format plugin:

  • Multiple Format Storage: Allow users to store multiple format “presets” and switch between them

  • Format Library: Create a dropdown that shows recently used formats

  • Selective Format Application: Add a modal that lets users choose which aspects of formatting to apply (colors only, fonts only, etc.)

  • Cross-Editor Sync: Store formats in localStorage to persist across page reloads or even different editor instances

  • Keyboard Shortcuts: Add hotkey support for power users (Ctrl+Shift+C to copy format, Ctrl+Shift+V to paste)

Each of these enhancements would follow the same architectural patterns we’ve explored today, demonstrating how Froala’s design scales from simple plugins to complex feature sets.

Conclusion: The Power of Extensibility

Building a custom Paint Format plugin for Froala Editor reveals something profound about the editor’s architecture: it’s not just a tool for editing content — it’s a platform for building editing experiences. The same APIs we used to build this plugin power every feature in Froala, from basic text formatting to complex table manipulation.

This architectural consistency means that as you master Froala’s plugin system, you’re not just learning how to add one feature — you’re learning how to add any feature. Need custom validation? Build a plugin. Want to integrate with your company’s design system? Build a plugin. Need to support a proprietary content format? You know what to do.

The modular architecture also means you’re never locked in.

This article was published on the Froala blog.

Comments 0 total

    Add comment