Visual Diff Merge Customization Guide

A powerful visual diff and merge tool for the web

Customization Overview

Visual Diff Merge offers several ways to customize its behavior and appearance:

  • Configuration Settings - Adjust behavior through api/config.php
  • Styling Customization - Modify CSS to change the appearance
  • Language and Translations - Customize text strings and add languages
  • Extending Functionality - Add custom components or modify existing ones
  • Integration with Other Systems - Use Visual Diff Merge in your own applications

Configuration Customization

The api/config.php file is the primary way to customize Visual Diff Merge's behavior. It controls both PHP (server-side) and JavaScript (client-side) settings.

For detailed information about the structure of the configuration file and how to set it up, refer to the Configuration section in the Installation guide.

Example Configuration

<?php

return [
    'php' => [
        'debug' => [
            'enabled' => true,  // Enable debugging
            'logLevel' => 3,    // Verbose logging
            'logFile' => 'custom-debug.log'  // Custom log file
        ],
        'diff' => [
            'contextLines' => 5,  // Show 5 lines of context
            'ignoreWhitespace' => true,  // Ignore whitespace changes
            'ignoreCase' => false
        ],
        'security' => [
            'csrfProtection' => true,
            'salt' => 'your-custom-salt-string',  // Custom security salt
            'allowedDirectories' => [
                realpath(__DIR__ . '/../diff-viewer/samples'),
                '/var/www/html/my-project'
            ]
        ]
    ],
    'javascript' => [
        'lang' => 'fr',  // Set French as default language
        'debug' => true,
        'logLevel' => 3,
        'theme' => [
            'defaultFamily' => 'github',  // Use GitHub theme family
            'defaultMode' => 'light',     // Default to light mode
            'showSelector' => true
        ],
        // Custom save options
        'saveOptions' => [
            'saveToOriginal' => true,
            'saveWithSuffix' => true,
            'saveToOld' => false,  // Disable saving to old file
            'saveToOldWithSuffix' => false,
            'saveToBoth' => true,
            'saveToBothWithSuffix' => true
        ]
    ]
];

Key Customization Options

Diff Behavior

  • php.diff.contextLines - Adjust the number of unchanged lines shown around changes. Lower values show less context, higher values show more context.
  • php.diff.ignoreWhitespace - Set to true to ignore whitespace changes when comparing content. Useful for code comparisons where formatting may differ.
  • php.diff.ignoreCase - Set to true to ignore case differences when comparing content.

Theme and UI

  • javascript.theme.defaultFamily - Change the default syntax highlighting theme family. Options include:
    • 'atom-one' - Atom One theme (default)
    • 'github' - GitHub theme
    • 'vs' - Visual Studio theme
    • 'xcode' - Xcode theme
    • And many others from highlight.js
  • javascript.theme.defaultMode - Set default theme mode:
    • 'dark' - Dark mode (default)
    • 'light' - Light mode
  • javascript.theme.showSelector - Control visibility of the theme selector. Set to false to hide it.

Save Options

Control which save options are available in the merge UI:

  • javascript.saveOptions.saveToOriginal - Save to current file (new file)
  • javascript.saveOptions.saveWithSuffix - Save to current file with suffix
  • javascript.saveOptions.saveToOld - Save to old file (overwrite)
  • javascript.saveOptions.saveToOldWithSuffix - Save to old file with suffix
  • javascript.saveOptions.saveToBoth - Save to both files (overwrite both)
  • javascript.saveOptions.saveToBothWithSuffix - Save to both files with suffix

Styling Customization

Visual Diff Merge's appearance can be customized through CSS. There are several approaches:

Approach 1: Override with Custom CSS

Add your own CSS file after loading the Visual Diff Merge CSS:

<!-- Load Visual Diff Merge CSS first -->
<link rel="stylesheet" href="path/to/diff-viewer.min.css">
<link rel="stylesheet" href="path/to/diff-viewer-theme.min.css">

<!-- Then load your custom CSS to override styles -->
<link rel="stylesheet" href="path/to/your-custom-styles.css">

Theme Customization

The diff-viewer-theme.min.css file contains styles for the diff viewer UI elements (buttons, alerts, etc.) that can be customized in two main ways:

  • Compile your own custom theme by modifying src/scss/theme.scss
  • Override the CSS classnames using the PHP configuration options to match your preferred CSS framework

Approach 2: Modify SCSS Source Files

If you're using the development setup, you can modify the SCSS source files directly:

  1. Modify files in src/scss/ or scss-shared/ directories
  2. Rebuild the CSS using the provided scripts:
    npm run compile-scss

Important CSS Classes

Here are some key CSS classes you may want to customize:

CSS Class Description
.vdm-diff__viewer Main diff viewer container
.vdm-diff__chunk Individual diff chunk
.vdm-diff__line--added Added lines
.vdm-diff__line--removed Removed lines
.vdm-diff__line--unchanged Unchanged lines
.vdm-diff__panel--left Left panel (old content)
.vdm-diff__panel--right Right panel (new content)
.vdm-btn Buttons
.vdm-loader Loading indicator container
.vdm-loader__spinner Loading spinner animation
.vdm-loader__message Loading message text
.vdm-d-none Utility class to hide elements (display: none)
.vdm-diff__navigation Navigation controls

Example Custom CSS

/* Custom colors for diff chunks */
.vdm-diff__line--added {
    background-color: rgba(0, 180, 0, 0.2);
}

.vdm-diff__line--removed {
    background-color: rgba(255, 0, 0, 0.2);
}

/* Custom font for code */
.vdm-diff__code {
    font-family: 'Fira Code', monospace;
    font-size: 14px;
}

/* Custom button styling */
.vdm-btn {
    border-radius: 4px;
    text-transform: uppercase;
    font-weight: bold;
}

/* Custom navigation controls */
.vdm-diff__navigation {
    background-color: #f5f5f5;
    border-bottom: 1px solid #ddd;
}

/* Highlight current chunk */
.vdm-diff__chunk.vdm-active {
    box-shadow: 0 0 0 2px #4285f4;
}

Language and Translations

Visual Diff Merge supports multiple languages through translation objects in the configuration.

Setting Default Language

Set the default language in api/config.php:

'javascript' => [
    'lang' => 'fr', // Set French as default language
    // Other settings...
]

Adding Custom Translations

To add or modify translations, edit the translations section in api/config.php:

'javascript' => [
    // Other settings...
    'translations' => [
        'en' => [
            'applyMerge' => 'Apply Merge',
            'continueResolving' => 'Continue Resolving',
            // Other English translations...
        ],
        'fr' => [
            'applyMerge' => 'Appliquer la fusion',
            'continueResolving' => 'Continuer la résolution',
            // Other French translations...
        ],
        'es' => [
            'applyMerge' => 'Aplicar fusión',
            'continueResolving' => 'Continuar resolviendo',
            // Other Spanish translations...
        ]
    ]
]

Adding a New Language

To add support for a new language:

  1. Create a new language key in the translations object
  2. Provide translations for all strings
  3. Set the javascript.lang value to your new language code
'javascript' => [
    'lang' => 'de', // Set German as default language
    'translations' => [
        // Existing translations...
        'de' => [
            'applyMerge' => 'Zusammenführung anwenden',
            'continueResolving' => 'Weiter auflösen',
            'filesIdenticalMessage' => '<strong>Dateien sind identisch</strong><br>Die verglichenen Dateien sind identisch. Keine Unterschiede gefunden.',
            // Add all other required translations
        ]
    ]
]

Required Translation Keys

The full list of translatable strings is available in api/config.example.php. At minimum, you should provide translations for:

  • UI elements (applyMerge, continueResolving, etc.)
  • Error messages (invalidRequestMethod, fileNotFound, etc.)
  • Loading states (loadingContent, processingChunks, etc.)
  • Confirmation dialogs (unresolvedConflicts, resolveConflictsMessage, etc.)

Extending Functionality

Visual Diff Merge can be extended in several ways to add custom functionality.

Creating Custom UI Components

Add custom UI elements to the diff viewer:

// Create a custom component
class CustomControl {
    constructor(options = {}) {
        this.container = options.container || document.body;
        this.diffViewer = options.diffViewer;
    }

    initialize() {
        // Create your custom UI element
        const customElement = document.createElement('div');
        customElement.classList.add('vdm-custom-control');
        customElement.innerHTML = `
            <button class="vdm-btn vdm-btn--primary">Custom Action</button>
        `;

        // Add event listener
        const button = customElement.querySelector('button');
        button.addEventListener('click', () => {
            // Do something with the diff viewer
            console.log('Custom action clicked');
            this.diffViewer.refreshView();
        });

        // Append to container
        this.container.appendChild(customElement);
        return true;
    }
}

// After initializing the diff viewer
document.addEventListener('DOMContentLoaded', function() {
    // Wait for diffViewer to be initialized
    const checkDiffViewer = setInterval(() => {
        if (window.diffViewer) {
            clearInterval(checkDiffViewer);

            // Create and initialize your custom component
            const customControl = new CustomControl({
                container: document.querySelector('.vdm-diff__controls'),
                diffViewer: window.diffViewer
            });

            customControl.initialize();
        }
    }, 100);
});

Extending Manager Classes

Extend the manager classes to add custom behavior:

// Extend the TextCompareManager class
class CustomTextCompareManager extends TextCompareManager {
    constructor(options = {}) {
        // Call parent constructor
        super(options);

        // Add custom properties
        this.customProperty = options.customProperty || 'default';
    }

    // Override a method
    async handleFormSubmit(event) {
        // Call the parent method first
        await super.handleFormSubmit(event);

        // Add custom behavior after form submission
        console.log('Form submitted with custom behavior');
        this.performCustomAction();
    }

    // Add a new method
    performCustomAction() {
        console.log('Performing custom action');
        // Your custom logic here
    }
}

// Replace the default manager with your custom one
document.addEventListener('DOMContentLoaded', function() {
    // Initialize with your custom manager
    window.textCompareManager = new CustomTextCompareManager({
        customProperty: 'custom value'
    });
});

Adding Post-Processing Hooks

Add custom post-processing to the diff result:

// Create a post-processing hook
function addCustomPostProcessing() {
    // Wait for diffViewer to be initialized
    const checkDiffViewer = setInterval(() => {
        if (window.diffViewer) {
            clearInterval(checkDiffViewer);

            // Store the original method
            const originalRenderDiff = window.diffViewer.renderDiff;

            // Override the method
            window.diffViewer.renderDiff = function() {
                // Call the original method
                const result = originalRenderDiff.apply(this, arguments);

                // Perform post-processing
                console.log('Performing post-processing on diff');

                // Example: Add line numbers to the gutter
                const gutters = document.querySelectorAll('.vdm-diff__gutter');
                gutters.forEach(gutter => {
                    const lineNumbers = gutter.querySelectorAll('.vdm-diff__line-number');
                    lineNumbers.forEach((lineNumber, index) => {
                        lineNumber.setAttribute('data-custom-index', index);
                    });
                });

                return result;
            };
        }
    }, 100);
}

// Add the hook
document.addEventListener('DOMContentLoaded', addCustomPostProcessing);

Integration with Other Systems

Visual Diff Merge can be integrated with other systems and applications.

Embedding in Your Application

To embed Visual Diff Merge in your application:

<!-- Include required CSS files -->
<link rel="stylesheet" href="path/to/diff-viewer.min.css">
<link rel="stylesheet" href="path/to/diff-viewer-theme.min.css">

<!-- Create container for the diff viewer -->
<div id="my-diff-container"></div>

<!-- Include required JavaScript files -->
<script src="path/to/diff-viewer.min.js"></script>

<script>
    // Initialize the diff viewer with your content
    document.addEventListener('DOMContentLoaded', function() {
        // Configuration object
        const config = {
            container: 'my-diff-container',
            debug: false,
            logLevel: 1,
            old: {
                type: 'text',
                content: 'Old content here',
                filename: 'old.txt'
            },
            new: {
                type: 'text',
                content: 'New content here',
                filename: 'new.txt'
            }
        };

        // Process diff on the server
        fetch('path/to/api/diff-processor.php', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(config)
        })
        .then(response => response.json())
        .then(result => {
            if (result.success) {
                // Merge the result config with our initial config
                const mergedConfig = {...config, ...result.config};

                // Initialize diff viewer
                window.diffViewer = new DiffViewer(mergedConfig);
                window.diffViewer.initialize();
            } else {
                console.error('Error processing diff:', result.error);
            }
        });
    });
</script>

Creating Custom API Endpoints

Create custom API endpoints to integrate with your system:

<?php
// custom-integration.php

require_once __DIR__ . '/../vendor/autoload.php';

use VisualDiffMerge\Config;
use VisualDiffMerge\DiffViewer;
use VisualDiffMerge\PathManager;

// Initialize config
Config::init();

// Set JSON response headers
header('Content-Type: application/json');

// Get data from your system
$oldVersion = yourSystem->getVersion('old');
$newVersion = yourSystem->getVersion('current');

// Create a DiffViewer instance
$diffViewer = new DiffViewer([
    'debug' => Config::isDebugEnabled(),
    'diffContextLines' => Config::get('php.diff.contextLines'),
    'ignoreWhitespace' => Config::get('php.diff.ignoreWhitespace'),
    'ignoreCase' => Config::get('php.diff.ignoreCase')
]);

try {
    // Process the diff
    $diffData = $diffViewer->compareContent(
        $oldVersion->content,
        $newVersion->content,
        $newVersion->fileExtension
    );

    // Create secure reference IDs
    $pathManager = PathManager::getInstance();
    $oldRefId = $pathManager->registerPath($oldVersion->filePath);
    $newRefId = $pathManager->registerPath($newVersion->filePath);

    // Prepare the response
    $response = [
        'success' => true,
        'config' => [
            'diffData' => $diffData,
            'serverSaveEnabled' => true,
            'fileRefId' => $newRefId,
            'oldFileRefId' => $oldRefId,
            'newFileName' => basename($newVersion->filePath),
            'oldFileName' => basename($oldVersion->filePath),
            'translations' => Config::getTranslations(Config::get('javascript.lang'))
        ]
    ];

    echo json_encode($response);
} catch (Exception $e) {
    echo json_encode([
        'success' => false,
        'error' => $e->getMessage()
    ]);
}

Callbacks and Events

Integrate with your system using callbacks:

// Set up event listeners after DiffViewer is initialized
document.addEventListener('DOMContentLoaded', function() {
    // Wait for diffViewer to be initialized
    const checkDiffViewer = setInterval(() => {
        if (window.diffViewer) {
            clearInterval(checkDiffViewer);

            // Listen for chunk selection
            document.addEventListener('vdm:chunkSelected', function(event) {
                console.log('Chunk selected:', event.detail);

                // Integrate with your system
                yourSystem.notifyChunkSelected(event.detail.chunkId, event.detail.side);
            });

            // Listen for merge completion
            document.addEventListener('vdm:mergeCompleted', function(event) {
                console.log('Merge completed:', event.detail);

                // Integrate with your system
                yourSystem.notifyMergeCompleted(event.detail.files);

                // Navigate to a different page
                setTimeout(() => {
                    window.location.href = '/your-system/view?file=' + event.detail.files[0].filename;
                }, 2000);
            });
        }
    }, 100);
});

Creating Plugins

Create plugins to extend Visual Diff Merge with reusable functionality:

Plugin Architecture

Follow this structure for Visual Diff Merge plugins:

// Visual Diff Merge Plugin Template
(function(window) {
    'use strict';

    // Plugin constructor
    function VDMPlugin(options = {}) {
        this.name = 'MyCustomPlugin';
        this.version = '1.0.0';
        this.options = options;
        this.diffViewer = null;
    }

    // Plugin prototype
    VDMPlugin.prototype = {
        /**
         * Initialize the plugin
         * @param {DiffViewer} diffViewer - The DiffViewer instance
         * @returns {boolean} Success status
         */
        initialize: function(diffViewer) {
            if (!diffViewer) {
                console.error('DiffViewer instance is required');
                return false;
            }

            this.diffViewer = diffViewer;
            console.log(`${this.name} v${this.version} initialized`);

            // Set up your plugin functionality
            this.setupUI();
            this.attachEventListeners();

            return true;
        },

        /**
         * Set up plugin UI
         */
        setupUI: function() {
            // Create and insert your UI elements
            const pluginContainer = document.createElement('div');
            pluginContainer.classList.add('vdm-plugin-container');
            pluginContainer.innerHTML = `
                <div class="vdm-plugin-header">${this.name}</div>
                <div class="vdm-plugin-content">
                    <button class="vdm-btn vdm-plugin-btn">Plugin Action</button>
                </div>
            `;

            // Insert into the diff viewer UI
            const targetContainer = document.querySelector('.vdm-diff__controls');
            if (targetContainer) {
                targetContainer.appendChild(pluginContainer);
            }
        },

        /**
         * Attach event listeners
         */
        attachEventListeners: function() {
            // Find your plugin button
            const button = document.querySelector('.vdm-plugin-btn');
            if (button) {
                button.addEventListener('click', this.handleAction.bind(this));
            }
        },

        /**
         * Handle plugin action
         * @param {Event} event - Click event
         */
        handleAction: function(event) {
            console.log('Plugin action triggered');

            // Example: Highlight all modified chunks
            const modifiedChunks = this.diffViewer.getDiffData().chunks.filter(
                chunk => chunk.type === 'modified'
            );

            modifiedChunks.forEach(chunk => {
                // Do something with the chunk
                console.log('Modified chunk:', chunk);
            });

            // Custom logic here
        }
    };

    // Register plugin with Visual Diff Merge
    window.registerVDMPlugin = function(diffViewer, options) {
        const plugin = new VDMPlugin(options);
        plugin.initialize(diffViewer);
        return plugin;
    };
})(window);

// Usage:
// After DiffViewer is initialized
document.addEventListener('DOMContentLoaded', function() {
    // Wait for diffViewer to be initialized
    const checkDiffViewer = setInterval(() => {
        if (window.diffViewer) {
            clearInterval(checkDiffViewer);

            // Register the plugin
            const plugin = window.registerVDMPlugin(window.diffViewer, {
                // Plugin options
                option1: 'value1',
                option2: 'value2'
            });
        }
    }, 100);
});