Redactor 4 Vendor Customizations

This document tracks all customizations made to the Redactor 4 vendor files.
Review and re-apply these when upgrading Redactor.

Vendor Location: client/vendor/redactor4/
Current Version: 4.6.2 (with patches)
Last Updated: 2026-07-25
Patches By: Roman Mateja-Cabello (original), updated by AI assistant


Customization 1: Source Mode Scroll Jump Fix

File: redactor.js
Location: SourceModule._setHeight() and _adjustHeight()
Type: Bug fix
Status: Re-applied 2026-07-25 (4.6.2 upgrade; upstream still unpatched)

Problem

In Redactor source (HTML/CodeMirror) mode, _setHeight() (called on every CM 'change')
and _adjustHeight() did height = 'auto' then px on the backing <textarea>.
Even though CM hides the textarea and sizes itself, the style mutation + any
ancillary scroll/offset reads were causing the main viewport to jump to the top
on every keystroke, followed by a soft scroll back to the caret. Extremely
disruptive when editing long blog posts.

This also happened on open (adjust) and was exacerbated after vendor upgrades
that dropped prior patches.

Solution

  • Guard _setHeight(): when CodeMirror is active, do nothing (CM owns the
    viewport/scrolling for source).
  • In _adjustHeight() (open time): measure required height with an off-DOM
    clone so the live element is never temporarily set to 'auto'.
  • Never force window.scrollTo(0, ...) restores on the hot path.

Relevant Code (current patched)

_adjustHeight(editor) {
    const minHeight = this._getMinHeight();
    const el = this.$source.get();
    // Measure without mutating the live element (prevents scroll jump on open)
    const clone = el.cloneNode(true);
    clone.style.cssText = 'position:absolute;visibility:hidden;height:auto;width:' + el.offsetWidth + 'px;overflow:hidden;';
    el.parentNode.appendChild(clone);
    const contentHeight = clone.scrollHeight;
    clone.remove();
    const height = Math.max(contentHeight, minHeight);

    el.style.height = height + 'px';
    return Math.max(editor.height(), minHeight);
}

_setHeight() {
    const cm = this.app.codemirror && this.app.codemirror.cm;
    if (cm) {
        // CodeMirror manages its own viewport and scrolling for the source view.
        return;
    }
    this.$source.get().style.height = 'auto';
    this.$source.get().style.height = `${this.$source.get().scrollHeight}px`;
}

Upgrade Check

After any Redactor upgrade, ensure:

  • _setHeight() short-circuits when this.app.codemirror.cm exists.
  • _adjustHeight() uses a hidden clone for measurement rather than mutating the live .rx-source textarea.

Customization 2: Block Focus Undefined Guard

File: redactor.js
Location: BlockModule focus handling (around line 21525)
Type: Bug fix
Status: OBSOLETE — upstream since 4.6.x; 4.6.2 adds its own if (!this.instance?.getBlock) return; guard. No re-application needed.

Problem

After reassigning this.instance = this.instance.getParent(), the instance
could be undefined, causing errors.

Solution

Add a guard clause after the reassignment.

Patched Code

// Guard against undefined instance after reassignment (bug fix)
if (!this.instance) return;

Upgrade Check

Search for this.instance.getParent() and verify proper null handling exists.


Customization 3: Image Inline-Block Option

File: redactor.js
Location: ImageManager and block.image (multiple locations)
Type: Feature addition (WarmlyYours specific)
Status: DROPPED since the 4.6.0 upgrade (commit 6311f859d3) — not re-applied in 4.6.1/4.6.2, no user reports since. Resurrect from client/vendor/redactor4-backup-20260126/ only if requested.

Purpose

Adds an "Inline block (for centering)" checkbox to the image properties dialog.
This allows users to set display: inline-block on images for centering purposes.

Changes Required

  1. ImageManager form items (~line 23170):
inlineBlock: { type: 'checkbox', text: 'Inline block (for centering)', observer: 'image.observeInlineBlock', auto: true },
  1. ImageManager observer (~line 23262):
observeInlineBlock(obj) {
    // Always show the inline-block option (WY custom feature)
    return obj;
}
  1. Block image props (~line 25623):
inlineBlock: { getter: 'getInlineBlock', setter: 'setInlineBlock' },
  1. Block image methods (~line 25684):
getInlineBlock() {
    const display = this.$image.css('display');
    return display === 'inline-block';
},
setInlineBlock(value) {
    if (value) {
        this.$image.css('display', 'inline-block');
    } else {
        this.$image.css('display', '');
        if (this.$image.attr('style') === '') {
            this.$image.removeAttr('style');
        }
    }
},

Customization 4: Email Container Centering Fix

File: plugins/email/email.js
Location: _buildMainEmail() (around line 1097)
Type: Bug fix / Email compatibility
Status: Applied in 4.5.3

Problem

The email plugin's _buildMainEmail() creates the 600px container table but doesn't
add centering attributes. While the parent <td> has align="center", some email
clients (especially Apple Mail) don't properly center nested tables without explicit
centering on the table itself.

Solution

Add align="center" attribute and margin: 0 auto CSS to the email-container table.

Original Code

const $container = this.dom('<table>');
$container.addClass('email-container');
$container.attr({
    'width': widthAttr,
    'cellpadding': '0',
    'cellspacing': '0',
    'role': 'presentation'
});

Patched Code

const $container = this.dom('<table>');
$container.addClass('email-container');
$container.css({ 'margin': '0 auto' });
$container.attr({
    'width': widthAttr,
    'cellpadding': '0',
    'cellspacing': '0',
    'align': 'center',
    'role': 'presentation'
});

Upgrade Check

Search for _buildMainEmail in the email plugin. If the container table doesn't have
align: 'center' and margin: 0 auto, this patch is still needed.


Upgrade Procedure

1. Backup current vendor files

cp -r client/vendor/redactor4 client/vendor/redactor4-backup-$(date +%Y%m%d)

2. Copy new version files

# Core Redactor
cp extras/redactor/redactor-X-X-X/redactor.js client/vendor/redactor4/
cp extras/redactor/redactor-X-X-X/redactor.css client/vendor/redactor4/
cp extras/redactor/redactor-X-X-X/redactor.min.js client/vendor/redactor4/
cp extras/redactor/redactor-X-X-X/redactor.min.css client/vendor/redactor4/

# Plugins (copy folders as needed)
cp -r extras/redactor/redactor-X-X-X/plugins/* client/vendor/redactor4/plugins/

# Updated email plugin (if separate)
cp extras/redactor/email/* client/vendor/redactor4/plugins/email/

# Updated AI plugin (if separate)
cp extras/redactor/ai/* client/vendor/redactor4/plugins/ai/

3. Re-apply customizations

For each customization above:

  1. Check if the issue is fixed in the new version
  2. If not fixed, apply the patch manually
  3. Update line numbers in this document

4. Test checklist

  • Source mode toggle (no scroll jump)
  • Image inline-block option works
  • Email templates display correctly in editor
  • Email templates render correctly when sent
  • Blog editor works with all plugins (AI, FAQ, Video, Snippets)
  • Image insertion works (WY Image plugin)
  • Video insertion works (WY Video plugin)
  • No console errors

5. Update this document

  • New version number
  • Updated line numbers for patches
  • Any new customizations added

Future Consideration: Extractable Patches

Scroll Jump Fix (Could be runtime-patched)

This fix modifies an internal method and could potentially be applied at runtime
after Redactor loads. This would avoid modifying vendor files:

// Hypothetical runtime patch in redactor4.js
// Note: Requires understanding Redactor's module structure

// After Redactor is loaded, patch the SourceModule
const originalSetHeight = Redactor.modules?.Source?.prototype?._setHeight;
if (originalSetHeight) {
  Redactor.modules.Source.prototype._setHeight = function() {
    const el = this.$source.get();
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
    
    const clone = el.cloneNode(true);
    clone.style.cssText = `position:absolute;visibility:hidden;height:auto;width:${el.offsetWidth}px;overflow:hidden`;
    el.parentNode.appendChild(clone);
    const newHeight = clone.scrollHeight;
    clone.remove();
    
    if (newHeight !== el.offsetHeight) {
      el.style.height = `${newHeight}px`;
    }
    window.scrollTo(0, scrollTop);
  };
}

Caveat: Redactor's internal structure may change between versions, making this
fragile. Direct patching is currently more reliable.

Inline-Block Feature (Could be a plugin)

The inline-block checkbox feature could potentially be extracted into a standalone
Redactor plugin (wyinlineblock.js) that extends the image block's behavior.
This would require:

  1. Understanding Redactor's plugin API for extending existing blocks
  2. Creating a proper plugin structure
  3. Testing compatibility with each Redactor version

This is a larger effort but would make upgrades trivial.


  • Redactor config: client/js/crm/editors/redactor4.js
  • Stimulus controller: app/javascript/controllers/redactor4_init_controller.js
  • WY Image plugin: client/js/common/wyimage4.js
  • WY Video plugin: client/js/common/wyvideo4.js
  • WY FAQ plugin: client/js/common/wyfaq4.js
  • WY Embed plugin: client/js/common/wyembed4.js
  • WY Email Blocks plugin: client/js/common/wyemailblocks.js