Viewing File: /usr/local/cpanel/base/frontend/jupiter/filemanager/editors/html_editor.js

/*
#                                     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;
        },

    };

})();
Back to Directory File Manager