/*
# Copyright 2025 WebPros International, LLC
# All rights reserved.
# copyright@cpanel.net http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
*/
/* --------------------------*/
/* DEFINE GLOBALS FOR LINT */
/*--------------------------*/
/* global CPANEL: true */
/* global YAHOO: true */
/* global Jodit: true */
/* --------------------------*/
// Ensure CPANEL exists
if (typeof CPANEL === 'undefined') {
window.CPANEL = {};
}
CPANEL.joditEditor = (function () {
/* --------------------*/
/* Application State */
/* --------------------*/
var appData;
var joditInstance;
/**
* Initialize Jodit editor
*
* @method initialize
* @param {Object} config configuration object for Jodit
* @param {Object} data additional data for editor (file metadata, etc.)
*/
var initialize = function (config, data) {
appData = data;
// Get the textarea element
var textareaEl = document.getElementById(config.elementId || 'jodit-editor');
if (!textareaEl) {
console.error('Jodit editor element not found:', config.elementId || 'jodit-editor');
return;
}
// Clear the textarea first to ensure clean state
textareaEl.value = '';
joditInstance = Jodit.make(textareaEl, config.joditOptions || {});
// Set the content from appData after Jodit is fully initialized
setTimeout(function () {
if (appData && appData.fileContentInfo && appData.fileContentInfo.content) {
var content = appData.fileContentInfo.content;
content = content.replace(/(src|href)="([^"]+)"/g, function (match, attr, url) {
// Rewrite URLs to use the new base URL
if (url.startsWith(appData.baseUrl)) {
var newUrl = url.replace(appData.baseUrl, appData.baseUrlForEditor);
return `${attr}="${newUrl}"`;
}
return match;
});
// Set content using a single, reliable method to avoid duplication
try {
// Ensure editor is ready and clear any existing content
if (joditInstance && joditInstance.isReady) {
joditInstance.value = content;
} else {
// If editor is not ready, wait a bit more and try again
setTimeout(function () {
if (joditInstance) {
joditInstance.value = content;
}
}, 200);
}
} catch (e) {
console.error('Error setting content:', e);
// Fallback: try setting via textarea
var textarea = document.getElementById(config.elementId || 'jodit-editor');
if (textarea) {
textarea.value = content;
}
}
// Verify the content was set (simple check without additional manipulation)
setTimeout(function () {
var currentContent = joditInstance.value || '';
if (currentContent.length === 0 && content.length > 0) {
console.warn('Content not set properly - this may indicate an initialization issue');
}
}, 300);
} else {
// Set empty content if no data is available
joditInstance.value = '';
}
}, 150);
// Set up event handlers
setupEventHandlers();
// Show the form if it was hidden
showEditorForm();
};
/**
* Set up event handlers for the Jodit editor
*
* @method setupEventHandlers
*/
function setupEventHandlers() {
if (!joditInstance) return;
// Set up keyboard shortcuts
joditInstance.e.on('keydown', function (e) {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveFile();
}
});
// Track dirty state when content changes
joditInstance.__originalContent = joditInstance.value;
joditInstance.__isDirty = false;
joditInstance.__saveInProgress = false;
joditInstance.__reEnabling = false;
// Use a debounced change handler to avoid false positives
var changeTimeout;
var changeHandler = function () {
// Ignore changes during save operations or re-enabling
if (joditInstance && !joditInstance.__saveInProgress && !joditInstance.__reEnabling) {
// Clear any existing timeout
if (changeTimeout) {
clearTimeout(changeTimeout);
}
// Set a small delay to avoid marking dirty during save operations
changeTimeout = setTimeout(function () {
if (!joditInstance.__saveInProgress && !joditInstance.__reEnabling) {
joditInstance.__isDirty = true;
}
}, 100);
}
};
// Store the change handler for later use
joditInstance.__changeHandler = changeHandler;
joditInstance.e.on('change', changeHandler);
}
/**
* Show the editor form
*
* @method showEditorForm
*/
function showEditorForm() {
var form = document.getElementById(appData.formID || 'htmlEditorForm');
if (form) {
form.className = form.className.replace('cpanelHide', 'cpanelShow');
}
}
/**
* Disable the editor during save operations
*
* @method disableEditor
*/
function disableEditor() {
if (!joditInstance) return;
try {
// Method 1: Set readonly mode
joditInstance.setReadOnly(true);
// Method 2: Disable the editor container
var editorContainer = joditInstance.container;
if (editorContainer) {
editorContainer.style.pointerEvents = 'none';
editorContainer.style.opacity = '0.6';
editorContainer.setAttribute('data-saving', 'true');
}
// Method 3: Disable toolbar buttons
var toolbar = joditInstance.toolbar;
if (toolbar && toolbar.container) {
toolbar.container.style.pointerEvents = 'none';
}
} catch (e) {
// Could not disable editor
}
}
/**
* Enable the editor after save operations
*
* @method enableEditor
*/
function enableEditor() {
if (!joditInstance) {
return;
}
try {
// Temporarily remove change event handler to prevent false dirty marking
if (joditInstance.__changeHandler) {
joditInstance.e.off('change', joditInstance.__changeHandler);
}
// Temporarily disable change event handling during re-enable
joditInstance.__reEnabling = true;
// Method 1: Remove readonly mode
joditInstance.setReadOnly(false);
// Method 2: Re-enable the editor container
var editorContainer = joditInstance.container;
if (editorContainer) {
editorContainer.style.pointerEvents = '';
editorContainer.style.opacity = '';
editorContainer.removeAttribute('data-saving');
}
// Method 3: Re-enable toolbar buttons
var toolbar = joditInstance.toolbar;
if (toolbar && toolbar.container) {
toolbar.container.style.pointerEvents = '';
}
// Re-add the change handler and clear flags after a delay to allow Jodit to settle
setTimeout(function () {
if (joditInstance) {
joditInstance.__reEnabling = false;
// Re-add the change event handler
if (joditInstance.__changeHandler) {
joditInstance.e.on('change', joditInstance.__changeHandler);
}
}
}, 300);
} catch (e) {
// Make sure to clear the flag and restore handler even on error
if (joditInstance) {
joditInstance.__reEnabling = false;
if (joditInstance.__changeHandler) {
joditInstance.e.on('change', joditInstance.__changeHandler);
}
}
}
}
/**
* Save the file content
*
* @method saveFile
*/
function saveFile() {
if (window.mixpanel) {
window.mixpanel.track('html-editor-file-save');
}
var Dynamic_Notice = CPANEL.ajax.Dynamic_Notice;
if (!joditInstance) {
notice = new Dynamic_Notice({
level: 'error',
content: LOCALE.maketext('Editor not initialized.')
});
return;
}
// Set save in progress flag to prevent dirty marking during save
joditInstance.__saveInProgress = true;
// Disable the editor during save.
// **NOTE**: We must do this BEFORE inspecting/copying content from the Jodit instance,
// otherwise, the content will have 'contenteditable=true' attributes injected (CPANEL-50329).
disableEditor();
var content = joditInstance.value;
if (!content || content.trim() === '') {
notice = new Dynamic_Notice({
level: 'warning',
content: LOCALE.maketext('No content to save.')
});
return;
}
// Show saving status
notice = new Dynamic_Notice({
level: 'info',
content: LOCALE.maketext('Saving file …')
});
updateSaveButton(true, LOCALE.maketext('Saving...'));
// Call the API to save the file with restored content
_saveFile(content);
}
/**
* API call to save the file content
*
* @method _saveFile
* @private
* @param {String} content File content
*/
function _saveFile(content) {
var Dynamic_Notice = CPANEL.ajax.Dynamic_Notice;
var fileMetaData = appData.fileMetaData;
var callback = {
success: function (response) {
_saveSuccess(response);
},
failure: function (response) {
_saveFailure(response);
}
};
// Add a timeout to prevent getting stuck
var saveTimeout = setTimeout(function () {
notice = new Dynamic_Notice({
level: 'error',
content: LOCALE.maketext('Save operation timed out.')
});
enableEditor();
updateSaveButton(false, LOCALE.maketext('Save File'));
if (joditInstance) {
joditInstance.__saveInProgress = false;
}
}, 30000);
// Store timeout so we can clear it on success/failure
joditInstance.__saveTimeout = saveTimeout;
// Check if CPANEL.api is available
if (typeof CPANEL === 'undefined' || typeof CPANEL.api !== 'function') {
clearTimeout(saveTimeout);
_saveFailure({ error: 'CPANEL.api is not available' });
return;
}
try {
content = content.replace(/(src|href)="([^"]+)"/g, function (match, attr, url) {
// Rewrite URLs back to using the original base URL
if (url.startsWith(appData.baseUrlForEditor)) {
var newUrl = url.replace(appData.baseUrlForEditor, appData.baseUrl);
return `${attr}="${newUrl}"`;
}
return match;
});
CPANEL.api({
version: 3,
module: "Fileman",
func: "save_file_content",
data: {
dir: fileMetaData.dirPath,
file: fileMetaData.fileName,
content: content,
to_charset: fileMetaData.charset,
fallback: true,
html: true
},
callback: callback
});
} catch (e) {
clearTimeout(saveTimeout);
_saveFailure({ error: 'API call failed: ' + e.message });
}
}
/**
* Success callback for save operation
*
* @method _saveSuccess
* @private
* @param {Object} o Response object
*/
function _saveSuccess(o) {
try {
var Dynamic_Notice = CPANEL.ajax.Dynamic_Notice;
// Clear the save timeout
if (joditInstance && joditInstance.__saveTimeout) {
clearTimeout(joditInstance.__saveTimeout);
joditInstance.__saveTimeout = null;
}
updateSaveButton(false, LOCALE.maketext('Save File'));
if (o && o.cpanel_status) {
notice = new Dynamic_Notice({
level: 'success',
content: LOCALE.maketext('File saved successfully!')
});
// Mark as clean and clear save-in-progress flag BEFORE re-enabling
if (joditInstance) {
// Clear save in progress flag first
joditInstance.__saveInProgress = false;
// Reset our custom dirty flag (this is what we actually check)
joditInstance.__isDirty = false;
// Update original content to current content
joditInstance.__originalContent = joditInstance.value;
}
// Re-enable the editor AFTER clearing dirty state
enableEditor();
} else {
var errorMsg = LOCALE.maketext('Failed to save file.');
if (o.cpanel_error) {
errorMsg += ': ' + o.cpanel_error;
}
notice = new Dynamic_Notice({
level: 'error',
content: errorMsg
});
enableEditor(); // Re-enable editor on error too
}
} catch (e) {
notice = new Dynamic_Notice({
level: 'error',
content: LOCALE.maketext('Error processing save response.')
});
enableEditor(); // Re-enable editor on error too
}
}
/**
* Failure callback for save operation
*
* @method _saveFailure
* @private
* @param {Object} o Response object
*/
function _saveFailure() {
var Dynamic_Notice = CPANEL.ajax.Dynamic_Notice;
// Clear the save timeout
if (joditInstance && joditInstance.__saveTimeout) {
clearTimeout(joditInstance.__saveTimeout);
joditInstance.__saveTimeout = null;
}
updateSaveButton(false, LOCALE.maketext('Save File'));
notice = new Dynamic_Notice({
level: 'error',
content: LOCALE.maketext('Network error while saving file. Please try again.')
});
// Re-enable the editor even on failure
enableEditor();
// Clear save in progress flag even on failure
if (joditInstance) {
joditInstance.__saveInProgress = false;
}
}
/**
* Preview the file content
*
* @method previewFile
*/
function previewFile() {
var Dynamic_Notice = CPANEL.ajax.Dynamic_Notice;
if (!joditInstance) {
notice = new Dynamic_Notice({
level: 'error',
content: LOCALE.maketext('Editor not initialized.')
});
return;
}
var content = joditInstance.value;
var previewWindow = window.open('', '_blank', 'width=800,height=600,scrollbars=yes,resizable=yes');
if (previewWindow) {
previewWindow.document.write(content);
previewWindow.document.close();
} else {
notice = new Dynamic_Notice({
level: 'error',
content: LOCALE.maketext('Could not open preview window. Please check popup blocker settings.')
});
}
}
/**
* Update save button state
*
* @method updateSaveButton
* @param {Boolean} disabled Whether the button should be disabled
* @param {String} text The button text
*/
function updateSaveButton(disabled, text) {
var saveButton = document.getElementById('save-button');
if (saveButton) {
saveButton.disabled = disabled;
saveButton.textContent = text;
}
}
/**
* Handle page unload to warn about unsaved changes
*
* @method handleBeforeUnload
*/
function handleBeforeUnload(e) {
// Only warn if we have our custom dirty flag set
// Ignore Jodit's internal dirty state as it can be unreliable
if (joditInstance && joditInstance.__isDirty === true) {
var confirmationMessage = 'You have unsaved changes. Are you sure you want to leave?';
e.returnValue = confirmationMessage;
return confirmationMessage;
}
// If no custom dirty flag, allow navigation without warning
}
// Set up beforeunload handler
window.addEventListener('beforeunload', handleBeforeUnload);
/**
* Force recovery from stuck save state
*
* @method forceRecovery
*/
function forceRecovery() {
var Dynamic_Notice = CPANEL.ajax.Dynamic_Notice;
if (joditInstance) {
// Clear all save-related flags
joditInstance.__saveInProgress = false;
joditInstance.__reEnabling = false;
// Clear any timeouts
if (joditInstance.__saveTimeout) {
clearTimeout(joditInstance.__saveTimeout);
joditInstance.__saveTimeout = null;
}
// Force enable the editor
enableEditor();
// Reset button state
updateSaveButton(false, LOCALE.maketext('Save File'));
notice = new Dynamic_Notice({
level: 'info',
content: LOCALE.maketext('Editor state reset.')
});
}
}
// Add UX helper methods to CPANEL.joditEditor
function showUploadProgress(files) {
if (typeof JoditUXHelpers !== 'undefined') {
return JoditUXHelpers.showProgress(files);
} else {
console.warn('JoditUXHelpers not available');
return [];
}
}
function updateUploadProgress(fileId, progress, status) {
if (typeof JoditUXHelpers !== 'undefined') {
JoditUXHelpers.updateProgress(fileId, progress, status);
} else {
console.warn('JoditUXHelpers not available');
}
}
function hideUploadProgress() {
if (typeof JoditUXHelpers !== 'undefined') {
JoditUXHelpers.hideProgress();
} else {
console.warn('JoditUXHelpers not available');
}
}
function showUploadToast(message, type) {
if (typeof JoditUXHelpers !== 'undefined') {
JoditUXHelpers.showToast(message, type);
} else {
console.warn('JoditUXHelpers not available');
}
}
// Public API
return {
initialize: initialize,
saveFile: saveFile,
previewFile: previewFile,
disableEditor: disableEditor,
enableEditor: enableEditor,
forceRecovery: forceRecovery,
showUploadProgress: showUploadProgress,
updateUploadProgress: updateUploadProgress,
hideUploadProgress: hideUploadProgress,
showUploadToast: showUploadToast,
getEditor: function () {
return joditInstance;
},
isDirty: function () {
return joditInstance && joditInstance.__isDirty === true;
},
};
})();