Skip to content

Conversation

@saifsultanc
Copy link
Contributor

Context

⛑️ Ticket(s): https://secure.helpscout.net/conversation/2987432785/86002

Summary

Primarily inspired from #1071, with some improvements. Takes into account the recent updates to the snippet here 55c2438

https://www.loom.com/share/fbb020ebea884e76a1ed2fe0eae1e9fb

@coderabbitai
Copy link

coderabbitai bot commented Jul 17, 2025

Walkthrough

Adds a persistent gwRichTextMode, editor readiness waiting, mode-aware initialization and switching for TinyMCE/HTML editors, content synchronization via a new window.SetFieldContentProperty API, and tweaks TinyMCE config (forced_root_block:false, height:250).

Changes

Cohort / File(s) Change Summary
Rich text mode & globals
gravity-forms/gw-rich-text-html-fields.php
Introduces global gwRichTextMode initialized from field.gwRichTextMode (default 'tmce') and persists mode changes via SetFieldProperty.
Editor readiness & switching
gravity-forms/gw-rich-text-html-fields.php
Adds waitForEditorToBeReady helper to delay actions until TinyMCE is ready, then initializes or switches editors based on gwRichTextMode, and handles removal via wp.editor.remove.
Content sync API & event bindings
gravity-forms/gw-rich-text-html-fields.php
Adds public window.SetFieldContentProperty to derive and set field content depending on current mode; binds editor change/paste events to update and persist content.
TinyMCE configuration tweaks
gravity-forms/gw-rich-text-html-fields.php
Adjusts TinyMCE init options: forced_root_block: false, height: 250; preserves content on editor changes and ensures mode-aware initialization timing.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Field Settings UI
    participant GFJS as gw-rich-text JS
    participant TinyMCE as TinyMCE Editor
    participant GFAPI as Gravity Forms Field API

    UI->>GFJS: load field (includes field.gwRichTextMode)
    GFJS->>GFJS: set gwRichTextMode (default 'tmce')
    GFJS->>TinyMCE: waitForEditorToBeReady()
    alt Editor becomes ready
        GFJS->>TinyMCE: initialize or switch editor mode (tmce/html)
        TinyMCE-->>GFJS: ready/initialized
    end
    TinyMCE->>GFJS: on change/paste events
    GFJS->>GFJS: call SetFieldContentProperty()
    GFJS->>GFAPI: SetFieldProperty(content)
    UI->>GFJS: user toggles mode
    GFJS->>GFJS: persist gwRichTextMode via SetFieldProperty
    GFJS->>TinyMCE: remove or re-init as needed (wp.editor.remove / init)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review focus:
    • waitForEditorToBeReady timing and edge cases.
    • Correct derivation and persistence of content in both modes (avoid data loss).
    • Proper use and cleanup of TinyMCE and wp.editor APIs.
    • Race conditions between init, mode switch, and event bindings.

Possibly related PRs

Suggested reviewers

  • veryspry

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: improvements to the gw-rich-text-html-fields.php snippet, which aligns with the substantive modifications detailed in the raw summary.
Description check ✅ Passed The description covers the required template sections with context (support ticket link), summary (inspiration from PR #1071 and recent updates), and additional reference materials (Loom video).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch saif/fix/86002-fix-gw-rich-text-html

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Jul 17, 2025

Warnings
⚠️ When ready, don't forget to request reviews on this pull request from your fellow wizards.

Generated by 🚫 dangerJS against 5916ad8

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
gravity-forms/gw-rich-text-html-fields.php (3)

63-66: Consider using a more scoped approach to avoid global variable pollution.

The global variable gwRichTextMode could potentially conflict with other scripts. Consider using a namespace or scoping it within an IIFE to avoid global pollution.

-		var gwRichTextMode;
+		var GWRichText = GWRichText || {};
 		jQuery( document ).on( 'gform_load_field_settings', function( event, field ) {
-			gwRichTextMode = field.gwRichTextMode || 'tmce';
+			GWRichText.mode = field.gwRichTextMode || 'tmce';

115-130: Improve timeout handling in the polling mechanism.

The polling mechanism is well-implemented, but the timeout case doesn't provide any fallback behavior or error handling. Consider adding a callback or logging when the timeout occurs.

 				const waitForEditorToBeReady = (callback, timeout = 5000) => {
 					const start = Date.now();
 					const interval = setInterval(() => {
 						const editor = typeof tinymce !== 'undefined' && tinymce.get(editorId);
 						if (editor) {
 							clearInterval(interval);
 							callback();
 						} else if (Date.now() - start > timeout) {
 							clearInterval(interval);
+							console.warn('TinyMCE editor initialization timed out');
 						}
 					}, 100);
 				};

131-148: Add error handling and consider scoping the global function.

The function logic is correct, but it lacks error handling for the TinyMCE editor instance and uses global scope.

 				// Set the content when save.
-				window.SetFieldContentProperty = function () {
+				window.SetFieldContentProperty = function () {
 					var mode = jQuery('#wp-' + editorId + '-wrap').hasClass('html-active') ? 'html' : 'tmce';
 					var content = '';

 					if (mode === 'html') {
 						content = jQuery('#' + editorId).val();
-					} else if (tinymce.get(editorId)) {
+					} else {
+						var editor = tinymce.get(editorId);
+						if (editor) {
-						content = tinymce.get(editorId).getContent();
+							content = editor.getContent();
+						}
 					}

 					SetFieldProperty('content', content);
 				};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec6b24f and 5e0d06d.

📒 Files selected for processing (1)
  • gravity-forms/gw-rich-text-html-fields.php (3 hunks)
🔇 Additional comments (2)
gravity-forms/gw-rich-text-html-fields.php (2)

150-158: Good improvement for mode state persistence.

The addition of saving the current mode to the field property ensures state persistence across form editor sessions. The implementation correctly identifies the clicked button and updates the field property accordingly.


129-129: Ensure consistent renaming of gwRichTextMode when adding a namespace

If you introduce a namespaced global (e.g. window.MyPlugin.richTextMode), be sure to update every reference in gravity-forms/gw-rich-text-html-fields.php:

  • Declaration:
    var gwRichTextMode;
  • Initialization from field settings:
    gwRichTextMode = field.gwRichTextMode || 'tmce';
  • Mode switch call:
    waitForEditorToBeReady(() =>
      window.switchEditors.go(editorId,
        gwRichTextMode === 'html' ? 'html' : 'tmce'
      )
    );
  • Saving back to the field property:
    SetFieldProperty('gwRichTextMode', mode)

Verify that each instance is updated to use your chosen namespace.

@spivurno
Copy link
Contributor

@saifsultanc A few issues:

  1. Code is being wrapped in tags automatically on save:
    https://www.loom.com/share/327f74651a0d4f2da0f3e45d74eda7e4

  2. Line breaks are being removed in the code editor.
    https://www.loom.com/share/d46948fae6f04518bf45a81442ebe5c8

    I believe this is an extension of the issue I reported originally where the code editor does not save values with line breaks. See the original Loom for details on that.

  3. The issue where the height of the code editor increases each time the field settings were saved is still present. See the original Loom for details.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e0d06d and a0c67e6.

📒 Files selected for processing (1)
  • gravity-forms/gw-rich-text-html-fields.php (1 hunks)
🔇 Additional comments (2)
gravity-forms/gw-rich-text-html-fields.php (2)

66-73: LGTM on editor cleanup logic.

The cleanup sequence correctly removes the TinyMCE instance first (if it exists), then calls wp.editor.remove() for complete cleanup before setting the new content value.


78-85: Verify forced_root_block: false is sufficient to prevent tag wrapping.

The PR comments report "code is being wrapped in tags automatically on save" despite forced_root_block: false. This setting should prevent <p> wrapping, but TinyMCE may still apply formatting through other mechanisms.

Consider adding these additional settings to prevent unwanted formatting:

 				wp.editor.initialize( id, {
 					tinymce: {
 						forced_root_block: false,
+						force_br_newlines: true,
+						remove_linebreaks: false,
+						convert_newlines_to_brs: false,
 						setup: function( editor ) {

Additionally, verify if the tag wrapping occurs on initial load, during editing, or specifically when saving the field settings. The Loom video in the PR comments may provide more context.

Comment on lines 76 to 88
setTimeout( function() {
wp.editor.initialize( id, {
tinymce: {
forced_root_block: false,
setup: function( editor ) {
editor.on( 'Paste Change input Undo Redo', function () {
SetFieldProperty( 'content', editor.getContent() );
} );
}
},
quicktags: true
} );
}, 100 );
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing editor_height likely causes height accumulation on each save.

The original editor initialization (line 38) specifies editor_height: 250, but this reinitialization omits it. This matches the reported issue where "code editor height increases each time the field settings are saved."

 			setTimeout( function() {
 				wp.editor.initialize( id, {
 					tinymce: {
 						forced_root_block: false,
+						height: 250,
 						setup: function( editor ) {
 							editor.on( 'Paste Change input Undo Redo', function () {
 								SetFieldProperty( 'content', editor.getContent() );
 							} );
 						}
 					},
 					quicktags: true
 				} );
 			}, 100 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTimeout( function() {
wp.editor.initialize( id, {
tinymce: {
forced_root_block: false,
setup: function( editor ) {
editor.on( 'Paste Change input Undo Redo', function () {
SetFieldProperty( 'content', editor.getContent() );
} );
}
},
quicktags: true
} );
}, 100 );
setTimeout( function() {
wp.editor.initialize( id, {
tinymce: {
forced_root_block: false,
height: 250,
setup: function( editor ) {
editor.on( 'Paste Change input Undo Redo', function () {
SetFieldProperty( 'content', editor.getContent() );
} );
}
},
quicktags: true
} );
}, 100 );
🤖 Prompt for AI Agents
In gravity-forms/gw-rich-text-html-fields.php around lines 76 to 88, the
wp.editor.initialize call reinitializes the editor without the editor_height
option which causes the editor height to grow on each save; add editor_height:
250 (matching the original initialization) into the options object passed to
wp.editor.initialize so the height is fixed, e.g. include "editor_height: 250"
alongside tinymce and quicktags, ensuring the value is numeric and consistent
with the original initialization.

@spivurno
Copy link
Contributor

spivurno commented Dec 4, 2025

@saifsultanc Seeing some wonkiness here that's preventing me from testing the original issues: https://www.loom.com/share/2a344afec9604cfa90548f1db527da09

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gravity-forms/gw-rich-text-html-fields.php (1)

70-81: height should be editor_height at the top level of the options object.

The height: 250 is placed inside the tinymce config, but wp.editor.initialize() expects editor_height as a top-level option (sibling to tinymce and quicktags). This likely explains why the reported height accumulation issue persists.

 			wp.editor.initialize( id, {
 				tinymce: {
 					forced_root_block: false,
-					height: 250,
 					setup: function( editor ) {
 						editor.on( 'Paste Change input Undo Redo', function () {
 							SetFieldProperty( 'content', editor.getContent() );
 						} );
 					}
 				},
-				quicktags: true
+				quicktags: true,
+				editor_height: 250
 			} );
🧹 Nitpick comments (1)
gravity-forms/gw-rich-text-html-fields.php (1)

117-131: Timeout silently fails without fallback.

If the editor doesn't initialize within 5 seconds, the callback never executes and the mode switch silently fails. Consider logging a warning or falling back to a default behavior.

 				const waitForEditorToBeReady = (callback, timeout = 5000) => {
 					const start = Date.now();
 					const interval = setInterval(() => {
 						const editor = typeof tinymce !== 'undefined' && tinymce.get(editorId);
 						if (editor) {
 							clearInterval(interval);
 							callback();
 						} else if (Date.now() - start > timeout) {
 							clearInterval(interval);
+							console.warn('TinyMCE editor initialization timed out for', editorId);
 						}
 					}, 100);
 				};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0c67e6 and 47d20f4.

📒 Files selected for processing (1)
  • gravity-forms/gw-rich-text-html-fields.php (3 hunks)
🔇 Additional comments (1)
gravity-forms/gw-rich-text-html-fields.php (1)

63-66: Mode persistence initialization looks correct.

The global variable properly captures the saved mode preference with a sensible default to visual mode ('tmce').

Comment on lines +147 to +150
// Update the content.
jQuery(document).on('change', `#${editorId}`, function () {
window.SetFieldContentProperty();
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use input event instead of change for real-time sync; prevent duplicate handlers.

The change event only fires when the textarea loses focus, so keystrokes won't sync until the user clicks away. This could explain the reported line break issues. Additionally, this delegated handler isn't cleaned up on editor reinitialization, leading to duplicate handlers.

+			// Namespace the event for proper cleanup
+			jQuery(document).off('input.gwRichText', `#${editorId}`);
-			jQuery(document).on('change', `#${editorId}`, function () {
+			jQuery(document).on('input.gwRichText', `#${editorId}`, function () {
 				window.SetFieldContentProperty();
 			});
🤖 Prompt for AI Agents
In gravity-forms/gw-rich-text-html-fields.php around lines 147 to 150, the
delegated handler uses the 'change' event (which only fires on blur) and can be
bound repeatedly on reinitialization; switch to the 'input' event for real-time
keystroke syncing and prevent duplicate handlers by removing any existing
handler before binding (use a namespaced event or call off(...) with the same
selector/event signature prior to on(...)). Ensure the new binding targets the
correct element and that cleanup happens on editor teardown or before
reinitialization to avoid multiple listeners.

Comment on lines 152 to 160
// Switch to visual/text mode.
jQuery(`#wp-${editorId}-wrap .switch-tmce, #wp-${editorId}-wrap .switch-html`).on('click', function() {
var mode = jQuery(this).hasClass('switch-tmce') ? 'tmce' : 'html';

window.switchEditors.go(editorId, mode);

// Save the current mode to field property.
SetFieldProperty('gwRichTextMode', mode)
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Sync content before switching modes to prevent data loss.

When switching from HTML to visual mode (or vice versa), the current content should be saved first. Otherwise, unsaved changes in the active mode could be lost during the switch.

 				jQuery(`#wp-${editorId}-wrap .switch-tmce, #wp-${editorId}-wrap .switch-html`).on('click', function() {
 					var mode = jQuery(this).hasClass('switch-tmce') ? 'tmce' : 'html';
 
+					// Sync content before switching to preserve unsaved changes
+					window.SetFieldContentProperty();
+
 					window.switchEditors.go(editorId, mode);
 
 					// Save the current mode to field property.
-					SetFieldProperty('gwRichTextMode', mode)
+					SetFieldProperty('gwRichTextMode', mode);
 				});

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In gravity-forms/gw-rich-text-html-fields.php around lines 152-160, the click
handler switches editor modes without syncing the current editor content,
risking data loss; before calling window.switchEditors.go(editorId, mode) detect
the active mode and capture the current content (from tinyMCE when visual/tmce
is active, or from the textarea when HTML is active), write that content back
into the underlying textarea and/or the field value used by Gravity Forms, then
persist any mode property (SetFieldProperty('gwRichTextMode', mode)) and only
after syncing call switchEditors.go so the editor switch preserves unsaved
changes.

… Fields Snippet.

Disable HTML click on load and re-enable after page load.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
gravity-forms/gw-rich-text-html-fields.php (2)

161-164: Still using change event instead of input; handlers may accumulate.

This was flagged in a previous review: the change event only fires on blur, not during typing, which may cause the reported line break sync issues. Additionally, this delegated handler isn't cleaned up between field loads, leading to duplicate handlers.

+			// Clean up previous handlers
+			jQuery(document).off('input.gwRichText', `#${editorId}`);
-			jQuery(document).on('change', `#${editorId}`, function () {
+			jQuery(document).on('input.gwRichText', `#${editorId}`, function () {
 				window.SetFieldContentProperty();
 			});

166-174: Content not synced before mode switch; handlers may accumulate.

As flagged in a previous review: switching modes without first saving content can lose unsaved changes. Also, this click handler binds on every tinymce-editor-setup event without cleanup.

+			// Clean up previous handlers
+			jQuery(`#wp-${editorId}-wrap .switch-tmce, #wp-${editorId}-wrap .switch-html`).off('click.gwRichText');
-			jQuery(`#wp-${editorId}-wrap .switch-tmce, #wp-${editorId}-wrap .switch-html`).on('click', function() {
+			jQuery(`#wp-${editorId}-wrap .switch-tmce, #wp-${editorId}-wrap .switch-html`).on('click.gwRichText', function() {
 				var mode = jQuery(this).hasClass('switch-tmce') ? 'tmce' : 'html';
 
+				// Sync content before switching to preserve unsaved changes
+				window.SetFieldContentProperty();
+
 				window.switchEditors.go(editorId, mode);
 
 				// Save the current mode to field property.
-				SetFieldProperty('gwRichTextMode', mode)
+				SetFieldProperty('gwRichTextMode', mode);
 			});
🧹 Nitpick comments (3)
gravity-forms/gw-rich-text-html-fields.php (3)

63-75: Magic timeout and inline style manipulation are fragile.

The 800ms delay is arbitrary and may be insufficient on slower connections or excessive on fast ones. Consider toggling a CSS class instead of manipulating inline styles, and tying the re-enable to actual editor readiness rather than a fixed timeout.

+	<style>
+		.gf-html-container.gw-editor-loading {
+			pointer-events: none;
+			opacity: 0.6;
+		}
+	</style>
+
 	<script>
-		// Immediately disable HTML click.
-		jQuery( '.gf-html-container' )
-			.css( 'pointer-events', 'none' )
-			.css( 'opacity', '0.6' );
-
-		// Re-enable only after full load of page.
-		jQuery( window ).on( 'load', function() {
-			setTimeout( function() {
-				jQuery( '.gf-html-container' )
-					.css( 'pointer-events', 'auto' )
-					.css( 'opacity', '1' );
-			}, 800 );
-		});
+		// Disable interaction until editor is ready.
+		jQuery( '.gf-html-container' ).addClass( 'gw-editor-loading' );

Then remove the class when the editor is actually ready (inside waitForEditorToBeReady callback) rather than using a fixed timeout.


77-79: Global variable gwRichTextMode risks conflicts.

Consider namespacing this variable (e.g., window.gwRichText = { mode: ... }) to avoid potential conflicts with other scripts in the Gravity Forms admin environment.


131-145: Silent timeout failure may mask initialization issues.

The 5-second timeout silently aborts without logging or fallback behavior. Consider adding a console warning on timeout to aid debugging if the editor fails to initialize.

 				} else if (Date.now() - start > timeout) {
 					clearInterval(interval);
+					console.warn('GW Rich Text: Editor initialization timed out for', editorId);
 				}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47d20f4 and 5916ad8.

📒 Files selected for processing (1)
  • gravity-forms/gw-rich-text-html-fields.php (3 hunks)
🔇 Additional comments (1)
gravity-forms/gw-rich-text-html-fields.php (1)

84-95: LGTM!

The height: 250 addition addresses the previously reported height accumulation issue, and forced_root_block: false correctly prevents automatic <p> tag wrapping.

Comment on lines +147 to +159
// Set the content when save.
window.SetFieldContentProperty = function () {
var mode = jQuery('#wp-' + editorId + '-wrap').hasClass('html-active') ? 'html' : 'tmce';
var content = '';

if (mode === 'html') {
content = jQuery('#' + editorId).val();
} else if (tinymce.get(editorId)) {
content = tinymce.get(editorId).getContent();
}

SetFieldProperty('content', content);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential content loss if TinyMCE not ready in visual mode.

When mode === 'tmce' but tinymce.get(editorId) returns falsy, content remains an empty string and SetFieldProperty is still called, potentially wiping existing content. Consider falling back to the textarea value:

 				if (mode === 'html') {
 					content = jQuery('#' + editorId).val();
 				} else if (tinymce.get(editorId)) {
 					content = tinymce.get(editorId).getContent();
+				} else {
+					// Fallback to textarea if TinyMCE not ready
+					content = jQuery('#' + editorId).val();
 				}
🤖 Prompt for AI Agents
In gravity-forms/gw-rich-text-html-fields.php around lines 147 to 159, the
SetFieldContentProperty function can set content to an empty string when mode
=== 'tmce' but tinymce.get(editorId) is falsy, risking data loss; change the
logic so that when tinymce is not available you fall back to the textarea value
(jQuery('#' + editorId).val()) instead of leaving content empty, then call
SetFieldProperty('content', content) with that fallback value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants