HEX
Server: Apache/2.4.68 (Debian)
System: Linux as-cs-widget-demo-us-central1 6.1.0-44-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.164-1 (2026-03-09) x86_64
User: root (0)
PHP: 8.2.32
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/wp-recipe-maker/assets/js/admin-template/App.js
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';

import Api from 'Shared/Api';
import Helpers from './general/Helpers';
import Shortcodes from './general/shortcodes';

import Menu from './menu';
import Main from './main';

import '../../css/admin/template/layout.scss';

const isEnabledSetting = ( value, defaultValue = true ) => {
    if ( 'undefined' === typeof value || null === value ) {
        return defaultValue;
    }

    if ( 'boolean' === typeof value ) {
        return value;
    }

    if ( 'number' === typeof value ) {
        return 0 !== value;
    }

    if ( 'string' === typeof value ) {
        const normalized = value.trim().toLowerCase();

        if ( [ '0', 'false', 'off', 'no', '' ].includes( normalized ) ) {
            return false;
        }

        if ( [ '1', 'true', 'on', 'yes' ].includes( normalized ) ) {
            return true;
        }
    }

    return !! value;
};

class App extends Component {

    constructor(props) {
        super(props);

        this.state = {
            mode: 'manage',
            editing: false,
            shortcode: false,
            templates: JSON.parse(JSON.stringify(wprm_admin_template.templates)),
            template: false,
            historyPast: [],
            historyFuture: [],
            historyPastCapped: false,
            codeHistoryResetNotice: false,
            savingTemplate: false,
            sidebarCollapsed: false,
            initializingFromUrl: true,
            editingBlock: false,
            manageTemplateType: false,
            defaultTemplateUsages: wprm_admin_template.default_template_usages || {},
        }
        this.historyEnabled = isEnabledSetting( wprm_admin_template.undo_redo_history, true );
        
        // Track if we're updating URL ourselves to prevent re-parsing
        this.isUpdatingUrl = false;
        this.historyDebounceTimer = null;
        this.historyDebounceOpen = false;
        this.historyDebounceMs = 750;
        this.historyMaxSteps = 20;
        this.samePropertyMergeWindowMs = 5000;
        this.lastTemplatePropertyChange = {
            id: false,
            at: 0,
            baseTemplate: false,
        };
    }

    componentDidMount() {
        window.addEventListener( 'beforeunload', this.beforeWindowClose.bind(this) );
        
        // Parse URL to restore state
        this.parseUrlAndRestoreState();
    }

    componentDidUpdate(prevProps) {
        // If URL changed externally (e.g., browser back/forward), restore state
        // But skip if we're updating the URL ourselves
        if (prevProps.location.pathname !== this.props.location.pathname) {
            if (!this.isUpdatingUrl) {
                this.parseUrlAndRestoreState();
            } else {
                // Reset flag after React processes the URL change
                // Use setTimeout to ensure it happens after all updates
                setTimeout(() => {
                    this.isUpdatingUrl = false;
                }, 0);
            }
        }
    }

    parseUrlAndRestoreState() {
        const path = this.props.location.pathname || '/';
        const previousSessionKey = this.getCurrentSessionKey();
        const nextSessionKey = this.getSessionKeyForPath(path);
        const shouldResetHistory = previousSessionKey !== nextSessionKey;
        const historyResetState = shouldResetHistory ? {
            historyPast: [],
            historyFuture: [],
            historyPastCapped: false,
        } : {};

        if ( shouldResetHistory ) {
            this.closeHistoryDebounceWindow();
            this.resetLastTemplatePropertyChange();
        }
        
        // Parse URL path
        if (path === '/' || path === '/manage' || path.startsWith('/manage/')) {
            // Manage Templates overview
            // Parse: /manage/:type/:slug?
            let manageType = false;
            let manageSlug = false;
            
            if (path.startsWith('/manage/')) {
                const pathParts = path.replace('/manage/', '').split('/');
                const validTypes = ['recipe', 'snippet', 'roundup', 'favorites', 'import'];
                if (pathParts[0] && validTypes.includes(pathParts[0])) {
                    manageType = pathParts[0];
                    if (pathParts[1]) {
                        manageSlug = pathParts[1];
                    }
                }
            }
            
            // Validate that slug exists in templates if provided
            let finalTemplate = false;
            if (manageSlug && this.state.templates.hasOwnProperty(manageSlug)) {
                finalTemplate = manageSlug;
            }
            
            this.setState({
                mode: 'manage',
                editing: false,
                shortcode: false,
                template: finalTemplate ? JSON.parse(JSON.stringify(this.state.templates[finalTemplate])) : false,
                editingBlock: false,
                manageTemplateType: manageType,
                initializingFromUrl: false,
                ...historyResetState,
            });
        } else if (path.startsWith('/shortcode/')) {
            // Shortcode Generator - editing a specific shortcode
            const shortcodeId = path.replace('/shortcode/', '');
            const shortcode = this.findShortcodeById(shortcodeId);
            
            if (shortcode) {
                this.setState({
                    mode: 'shortcode',
                    editing: false,
                    shortcode: shortcode,
                    template: false,
                    editingBlock: false,
                    manageTemplateType: false,
                    initializingFromUrl: false,
                    ...historyResetState,
                });
            } else {
                // Invalid shortcode ID, redirect to shortcode generator
                this.updateUrl('/shortcode');
                this.setState({
                    mode: 'shortcode',
                    editing: false,
                    shortcode: false,
                    template: false,
                    editingBlock: false,
                    manageTemplateType: false,
                    initializingFromUrl: false,
                    ...historyResetState,
                });
            }
        } else if (path === '/shortcode') {
            // Shortcode Generator (no shortcode selected)
            this.setState({
                mode: 'shortcode',
                editing: false,
                shortcode: false,
                template: false,
                editingBlock: false,
                manageTemplateType: false,
                initializingFromUrl: false,
                ...historyResetState,
            });
        } else if (path === '/explorer') {
            // Feature Explorer
            this.setState({
                mode: 'feature-explorer',
                editing: false,
                shortcode: false,
                template: false,
                editingBlock: false,
                manageTemplateType: false,
                initializingFromUrl: false,
                ...historyResetState,
            });
        } else if (path.startsWith('/template/')) {
            // Editing a specific template
            // Parse: /template/:slug/:mode/:blockUid?
            const pathParts = path.replace('/template/', '').split('/');
            const slug = pathParts[0];
            const mode = pathParts[1] || 'properties'; // Default to properties
            const blockUid = pathParts[2] ? parseInt(pathParts[2], 10) : false;
            
            // Validate mode
            const validModes = ['properties', 'blocks', 'add', 'html', 'css'];
            const finalMode = validModes.includes(mode) ? mode : 'properties';
            
            // Only allow block UID for blocks mode
            const finalEditingBlock = ('blocks' === finalMode && blockUid !== false && !isNaN(blockUid)) ? blockUid : false;
            
            if (this.state.templates.hasOwnProperty(slug)) {
                const template = JSON.parse(JSON.stringify(this.state.templates[slug]));
                this.setState({
                    mode: finalMode,
                    editing: true,
                    shortcode: false,
                    template: template,
                    editingBlock: finalEditingBlock,
                    initializingFromUrl: false,
                    ...historyResetState,
                });
            } else {
                // Invalid template slug, redirect to manage
                this.updateUrl('/manage');
                this.setState({
                    mode: 'manage',
                    editing: false,
                    shortcode: false,
                    template: false,
                    editingBlock: false,
                    initializingFromUrl: false,
                    ...historyResetState,
                });
            }
        } else {
            // Unknown path, default to manage
            this.updateUrl('/manage');
            this.setState({
                mode: 'manage',
                editing: false,
                shortcode: false,
                template: false,
                editingBlock: false,
                manageTemplateType: false,
                initializingFromUrl: false,
                ...historyResetState,
            });
        }
    }

    cloneTemplate(template) {
        return JSON.parse(JSON.stringify(template));
    }

    isTemplateEqual(a, b) {
        return JSON.stringify(a) === JSON.stringify(b);
    }

    getCurrentSessionKey() {
        return this.state.editing && this.state.template && this.state.template.slug
            ? `template:${this.state.template.slug}`
            : false;
    }

    getSessionKeyForPath(path) {
        if ( path && path.startsWith('/template/') ) {
            const slug = path.replace('/template/', '').split('/')[0];

            return slug ? `template:${slug}` : false;
        }

        return false;
    }

    scheduleHistoryDebounceClose() {
        if ( this.historyDebounceTimer ) {
            clearTimeout(this.historyDebounceTimer);
        }

        this.historyDebounceTimer = setTimeout(() => {
            this.historyDebounceOpen = false;
            this.historyDebounceTimer = null;
        }, this.historyDebounceMs);
    }

    closeHistoryDebounceWindow() {
        if ( this.historyDebounceTimer ) {
            clearTimeout(this.historyDebounceTimer);
            this.historyDebounceTimer = null;
        }

        this.historyDebounceOpen = false;
    }

    trimHistoryStack(historyStack) {
        if ( historyStack.length > this.historyMaxSteps ) {
            return historyStack.slice( historyStack.length - this.historyMaxSteps );
        }

        return historyStack;
    }

    trimHistoryPast(historyPast, alreadyCapped = false) {
        const cappedNow = historyPast.length > this.historyMaxSteps;

        return {
            historyPast: this.trimHistoryStack(historyPast),
            historyPastCapped: alreadyCapped || cappedNow,
        };
    }

    resetHistory() {
        this.closeHistoryDebounceWindow();
        this.lastTemplatePropertyChange = {
            id: false,
            at: 0,
            baseTemplate: false,
        };
        this.setState({
            historyPast: [],
            historyFuture: [],
            historyPastCapped: false,
        });
    }

    resetLastTemplatePropertyChange() {
        this.lastTemplatePropertyChange = {
            id: false,
            at: 0,
            baseTemplate: false,
        };
    }

    shouldMergeWithPreviousTemplatePropertyChange(id, now = Date.now()) {
        const sameProperty = this.lastTemplatePropertyChange.id === id;
        const withinWindow = ( now - this.lastTemplatePropertyChange.at ) < this.samePropertyMergeWindowMs;
        const hasBaseTemplate = !! this.lastTemplatePropertyChange.baseTemplate;

        return sameProperty && withinWindow && hasBaseTemplate;
    }

    recordHistoryBeforeChange(prevState, { mode = 'immediate', persistUntilBoundary = false } = {}) {
        if ( ! this.historyEnabled ) {
            return {
                historyPast: [],
                historyFuture: [],
                historyPastCapped: false,
            };
        }

        if ( ! prevState.editing || ! prevState.template ) {
            return {
                historyPast: prevState.historyPast,
                historyFuture: prevState.historyFuture,
                historyPastCapped: prevState.historyPastCapped,
            };
        }

        let historyPast = prevState.historyPast;
        let historyPastCapped = prevState.historyPastCapped;
        const currentTemplateSnapshot = this.cloneTemplate(prevState.template);
        const lastHistoryTemplate = historyPast.length ? historyPast[historyPast.length - 1] : false;

        if ( 'debounced' === mode ) {
            if ( ! this.historyDebounceOpen ) {
                if ( ! lastHistoryTemplate || ! this.isTemplateEqual(lastHistoryTemplate, currentTemplateSnapshot) ) {
                    const trimmedHistoryPast = this.trimHistoryPast(historyPast.concat([currentTemplateSnapshot]), historyPastCapped);
                    historyPast = trimmedHistoryPast.historyPast;
                    historyPastCapped = trimmedHistoryPast.historyPastCapped;
                }
                this.historyDebounceOpen = true;
            }

            if ( persistUntilBoundary ) {
                if ( this.historyDebounceTimer ) {
                    clearTimeout(this.historyDebounceTimer);
                    this.historyDebounceTimer = null;
                }
            } else {
                this.scheduleHistoryDebounceClose();
            }
        } else {
            this.closeHistoryDebounceWindow();

            if ( ! lastHistoryTemplate || ! this.isTemplateEqual(lastHistoryTemplate, currentTemplateSnapshot) ) {
                const trimmedHistoryPast = this.trimHistoryPast(historyPast.concat([currentTemplateSnapshot]), historyPastCapped);
                historyPast = trimmedHistoryPast.historyPast;
                historyPastCapped = trimmedHistoryPast.historyPastCapped;
            }
        }

        const trimmedHistoryPast = this.trimHistoryPast(historyPast, historyPastCapped);

        return {
            historyPast: trimmedHistoryPast.historyPast,
            historyFuture: [],
            historyPastCapped: trimmedHistoryPast.historyPastCapped,
        };
    }

    onUndo() {
        if ( ! this.historyEnabled ) {
            return;
        }

        this.closeHistoryDebounceWindow();
        this.resetLastTemplatePropertyChange();

        this.setState((prevState) => {
            if ( ! prevState.template || ! prevState.historyPast.length ) {
                return null;
            }

            const restoredTemplate = prevState.historyPast[ prevState.historyPast.length - 1 ];

            return {
                template: this.cloneTemplate(restoredTemplate),
                historyPast: prevState.historyPast.slice(0, -1),
                historyFuture: this.trimHistoryStack(prevState.historyFuture.concat([ this.cloneTemplate(prevState.template) ])),
                historyPastCapped: prevState.historyPastCapped,
            };
        });
    }

    onRedo() {
        if ( ! this.historyEnabled ) {
            return;
        }

        this.closeHistoryDebounceWindow();
        this.resetLastTemplatePropertyChange();

        this.setState((prevState) => {
            if ( ! prevState.template || ! prevState.historyFuture.length ) {
                return null;
            }

            const restoredTemplate = prevState.historyFuture[ prevState.historyFuture.length - 1 ];
            const trimmedHistoryPast = this.trimHistoryPast(prevState.historyPast.concat([ this.cloneTemplate(prevState.template) ]), prevState.historyPastCapped);

            return {
                template: this.cloneTemplate(restoredTemplate),
                historyPast: trimmedHistoryPast.historyPast,
                historyFuture: prevState.historyFuture.slice(0, -1),
                historyPastCapped: trimmedHistoryPast.historyPastCapped,
            };
        });
    }

    findShortcodeById(shortcodeId) {
        // Try to find shortcode by ID in the current state
        if (this.state.shortcode && this.state.shortcode.id === shortcodeId) {
            return this.state.shortcode;
        }
        
        // If not found, create a basic shortcode object from the ID
        // This handles the case where we're navigating directly to a shortcode URL
        const { shortcodeKeysAlphebetically, contentShortcodes } = Shortcodes;
        
        if (shortcodeKeysAlphebetically.includes(shortcodeId)) {
            const needsClosingShortcode = contentShortcodes.includes(shortcodeId);
            return {
                uid: 0,
                id: shortcodeId,
                name: Helpers.getShortcodeName(shortcodeId),
                attributes: {},
                full: needsClosingShortcode ? `[${shortcodeId}][/${shortcodeId}]` : `[${shortcodeId}]`,
            };
        }
        
        return false;
    }

    updateUrl(path) {
        if (this.props.history) {
            this.isUpdatingUrl = true;
            this.props.history.push(path);
        }
    }
    
    componentWillUnmount() {
        this.closeHistoryDebounceWindow();
        window.removeEventListener( 'beforeunload', this.beforeWindowClose.bind(this) );
    }

    beforeWindowClose(event) {
        if ( this.changesMade() ) {
            return false;
        }
    }

    changesMade() {
        return this.state.editing &&
                ( this.state.template.html !== this.state.templates[this.state.template.slug].html
                || Helpers.parseCSS( this.state.template ) !== Helpers.parseCSS( this.state.templates[this.state.template.slug] ) );
    }

    onChangeEditing(editing) {
        if ( editing !== this.state.editing ) {
            // Scroll to top.
            window.scrollTo(0,0);

            if ( editing ) {
                // Update URL to template editing with current mode
                if (this.state.template && this.state.template.slug) {
                    const mode = this.state.mode || 'properties';
                    this.updateUrl(`/template/${this.state.template.slug}/${mode}`);
                }
                this.setState({
                    editing,
                    mode: 'properties',
                    editingBlock: false, // Reset editing block when starting to edit
                    historyPast: [],
                    historyFuture: [],
                    historyPastCapped: false,
                });
            } else {
                // Update URL to manage overview (preserve type and selected template if any)
                this.updateManageUrl(this.state.manageTemplateType, false);
                this.closeHistoryDebounceWindow();
                this.setState({
                    editing,
                    mode: 'manage',
                    template: false,
                    editingBlock: false,
                    historyPast: [],
                    historyFuture: [],
                    historyPastCapped: false,
                });
            }

            this.resetLastTemplatePropertyChange();
        }
    }

    updateManageUrl(type, slug) {
        if (type) {
            if (slug) {
                this.updateUrl(`/manage/${type}/${slug}`);
            } else {
                this.updateUrl(`/manage/${type}`);
            }
        } else {
            this.updateUrl('/manage');
        }
    }

    onChangeManageTemplateType(type) {
        if (type !== this.state.manageTemplateType) {
            // Clear selected template when changing type
            // Update state first, then URL in callback to avoid race condition
            this.setState({
                manageTemplateType: type,
                template: false,
            }, () => {
                this.updateManageUrl(type, false);
            });
        }
    }

    onChangeMode(mode) {
        // Handle clicking "Edit Blocks" while already in blocks mode and editing a specific block
        // In this case, clear the editing block to show the list of all blocks
        if ( 'blocks' === mode && 'blocks' === this.state.mode && this.state.editingBlock !== false && typeof this.state.editingBlock === 'number' ) {
            // Update URL to blocks list (without block UID)
            if ( this.state.editing && this.state.template && this.state.template.slug ) {
                this.updateUrl(`/template/${this.state.template.slug}/blocks`);
            }
            
            this.setState({
                editingBlock: false,
                // Expand sidebar when clicking on a menu item
                sidebarCollapsed: false,
            });
            return;
        }

        if ( mode !== this.state.mode ) {
            // Scroll to top when going to or coming from HTML/CSS mode.
            if ( 'html' === mode || 'html' === this.state.mode || 'css' === mode || 'css' === this.state.mode ) {
                window.scrollTo(0,0);
            }

            // Update URL for shortcode mode
            if ( 'feature-explorer' === mode ) {
                this.updateUrl('/explorer');
            } else if ( 'shortcode' === mode ) {
                if (this.state.shortcode && this.state.shortcode.id) {
                    this.updateUrl(`/shortcode/${this.state.shortcode.id}`);
                } else {
                    this.updateUrl('/shortcode');
                }
            } else if ( 'manage' === mode && !this.state.editing ) {
                // Preserve template type and selected template when navigating to manage
                this.updateManageUrl(this.state.manageTemplateType, this.state.template ? this.state.template.slug : false);
            } else if ( this.state.editing && this.state.template && this.state.template.slug ) {
                // Update URL with mode when editing a template
                const validModes = ['properties', 'blocks', 'add', 'html', 'css'];
                if (validModes.includes(mode)) {
                    // Reset editing block when switching modes (unless staying in blocks mode)
                    // Allow editingBlock to be 0 (first block) - only exclude false
                    const editingBlock = ('blocks' === mode && this.state.editingBlock !== false && typeof this.state.editingBlock === 'number') ? this.state.editingBlock : false;
                    if (editingBlock !== false) {
                        this.updateUrl(`/template/${this.state.template.slug}/${mode}/${editingBlock}`);
                    } else {
                        this.updateUrl(`/template/${this.state.template.slug}/${mode}`);
                    }
                }
            }

            this.setState((prevState) => {
                // Reset editing block when switching away from blocks mode
                // Allow editingBlock to be 0 (first block) - only exclude false
                const editingBlock = ('blocks' === mode && prevState.editingBlock !== false && typeof prevState.editingBlock === 'number') ? prevState.editingBlock : false;

                return {
                    mode,
                    template: 'feature-explorer' === mode ? false : prevState.template,
                    shortcode: 'feature-explorer' === mode ? false : prevState.shortcode,
                    editingBlock: editingBlock,
                    codeHistoryResetNotice: 'html' === mode || 'css' === mode ? false : prevState.codeHistoryResetNotice,
                    // Expand sidebar when clicking on a menu item
                    sidebarCollapsed: false,
                };
            });
        }
    }

    onChangeEditingBlock(uid) {
        if ( uid !== this.state.editingBlock ) {
            // Update URL when editing block changes (only in blocks mode)
            if ( this.state.editing && this.state.template && this.state.template.slug && 'blocks' === this.state.mode ) {
                // Allow uid 0 (first block) - only exclude false (no block selected)
                if (uid !== false && typeof uid === 'number') {
                    this.updateUrl(`/template/${this.state.template.slug}/blocks/${uid}`);
                } else {
                    this.updateUrl(`/template/${this.state.template.slug}/blocks`);
                }
            }
            
            this.setState({
                editingBlock: uid,
            });
        }
    }

    onToggleSidebar() {
        this.setState({
            sidebarCollapsed: !this.state.sidebarCollapsed,
        });
    }

    onChangeTemplate(slug) {
        // Don't do anything if we're in the middle of saving.
        if ( ! this.state.savingTemplate ) {
            const currentSlug = this.state.template && this.state.template.slug ? this.state.template.slug : false;
            const switchingTemplateWhileEditing = this.state.editing && currentSlug && currentSlug !== slug;

            if ( switchingTemplateWhileEditing ) {
                this.closeHistoryDebounceWindow();
                this.resetLastTemplatePropertyChange();
            }

            if (this.state.templates.hasOwnProperty(slug)) {
                const template = this.cloneTemplate(this.state.templates[slug]); // Important: use deep clone.
                
                // Update state first, then URL in callback to avoid race condition
                this.setState({
                    template: template,
                    editingBlock: false, // Reset editing block when switching templates
                    ...( switchingTemplateWhileEditing ? { historyPast: [], historyFuture: [], historyPastCapped: false } : {} ),
                }, () => {
                    // Update URL if we're editing this template
                    if (this.state.editing) {
                        const mode = this.state.mode || 'properties';
                        this.updateUrl(`/template/${slug}/${mode}`);
                    } else if (this.state.mode === 'manage') {
                        // Update URL in manage mode to include template type and selected template
                        this.updateManageUrl(this.state.manageTemplateType, slug);
                    }
                });
            } else {
                // If template not found and we're editing, go back to manage
                if (this.state.editing) {
                    this.closeHistoryDebounceWindow();
                    this.resetLastTemplatePropertyChange();
                    this.setState({
                        editing: false,
                        mode: 'manage',
                        template: false,
                        editingBlock: false,
                        historyPast: [],
                        historyFuture: [],
                        historyPastCapped: false,
                    }, () => {
                        this.updateManageUrl(this.state.manageTemplateType, false);
                    });
                } else if (this.state.mode === 'manage') {
                    // Update URL in manage mode when deselecting template
                    this.setState({
                        template: false,
                    }, () => {
                        this.updateManageUrl(this.state.manageTemplateType, false);
                    });
                } else {
                    this.setState({
                        template: false,
                    });
                }
            }
        }
    }

    onChangeTemplateProperty(id, value, options = {}) {
        if ( ! this.state.template || ! this.state.template.style || ! this.state.template.style.properties || ! this.state.template.style.properties[id] ) {
            return;
        }

        const historyMode = options.historyMode || 'immediate';
        const historyBoundary = !! options.historyBoundary;
        const historyPersistUntilBoundary = !! options.historyPersistUntilBoundary;
        const now = Date.now();
        const shouldMergeChange = this.historyEnabled && this.shouldMergeWithPreviousTemplatePropertyChange(id, now);
        const mergeBaseTemplate = shouldMergeChange && this.lastTemplatePropertyChange.baseTemplate
            ? this.cloneTemplate(this.lastTemplatePropertyChange.baseTemplate)
            : this.cloneTemplate(this.state.template);

        if ( value === this.state.template.style.properties[id].value ) {
            if ( historyBoundary ) {
                this.closeHistoryDebounceWindow();
            }
            return;
        }

        if ( this.historyEnabled ) {
            this.lastTemplatePropertyChange = {
                id,
                at: now,
                baseTemplate: mergeBaseTemplate,
            };
        } else {
            this.resetLastTemplatePropertyChange();
        }

        this.setState((prevState) => {
            if ( ! prevState.template || ! prevState.template.style || ! prevState.template.style.properties || ! prevState.template.style.properties[id] ) {
                return null;
            }

            if ( value === prevState.template.style.properties[id].value ) {
                return null;
            }

            const template = this.cloneTemplate(prevState.template);
            template.style.properties[id].value = value;

            return {
                ...( shouldMergeChange
                    ? (() => {
                        const lastHistoryTemplate = prevState.historyPast.length ? prevState.historyPast[ prevState.historyPast.length - 1 ] : false;
                        const isAtMergeBase = this.isTemplateEqual(template, mergeBaseTemplate);

                        if ( isAtMergeBase ) {
                            return {
                                historyPast: lastHistoryTemplate && this.isTemplateEqual(lastHistoryTemplate, mergeBaseTemplate)
                                    ? prevState.historyPast.slice(0, -1)
                                    : prevState.historyPast,
                                historyFuture: [],
                                historyPastCapped: prevState.historyPastCapped,
                            };
                        }

                        const trimmedHistoryPast = this.trimHistoryPast(prevState.historyPast.concat([ this.cloneTemplate(mergeBaseTemplate) ]), prevState.historyPastCapped);

                        return {
                            historyPast: lastHistoryTemplate && this.isTemplateEqual(lastHistoryTemplate, mergeBaseTemplate)
                                ? prevState.historyPast
                                : trimmedHistoryPast.historyPast,
                            historyFuture: [],
                            historyPastCapped: lastHistoryTemplate && this.isTemplateEqual(lastHistoryTemplate, mergeBaseTemplate)
                                ? prevState.historyPastCapped
                                : trimmedHistoryPast.historyPastCapped,
                        };
                    })()
                    : this.recordHistoryBeforeChange(prevState, { mode: historyMode, persistUntilBoundary: historyPersistUntilBoundary }) ),
                template,
            };
        }, () => {
            if ( historyBoundary ) {
                this.closeHistoryDebounceWindow();
            }
        });
    }

    onChangeFonts( fonts, options = {} ) {
        if ( ! this.state.template ) {
            return;
        }

        const historyPropertyId = options.historyPropertyId || '__fonts__';
        const historyMode = options.historyMode || 'immediate';
        const historyBoundary = !! options.historyBoundary;
        const now = Date.now();
        const shouldMergeChange = this.historyEnabled && this.shouldMergeWithPreviousTemplatePropertyChange(historyPropertyId, now);
        const mergeBaseTemplate = shouldMergeChange && this.lastTemplatePropertyChange.baseTemplate
            ? this.cloneTemplate(this.lastTemplatePropertyChange.baseTemplate)
            : this.cloneTemplate(this.state.template);

        if ( this.isTemplateEqual(fonts, this.state.template.fonts) ) {
            if ( historyBoundary ) {
                this.closeHistoryDebounceWindow();
            }
            return;
        }

        if ( this.historyEnabled ) {
            this.lastTemplatePropertyChange = {
                id: historyPropertyId,
                at: now,
                baseTemplate: mergeBaseTemplate,
            };
        } else {
            this.resetLastTemplatePropertyChange();
        }

        this.setState((prevState) => {
            if ( ! prevState.template || this.isTemplateEqual(fonts, prevState.template.fonts) ) {
                return null;
            }

            const template = this.cloneTemplate(prevState.template);
            template.fonts = [ ...fonts ];

            return {
                ...( shouldMergeChange
                    ? (() => {
                        const lastHistoryTemplate = prevState.historyPast.length ? prevState.historyPast[ prevState.historyPast.length - 1 ] : false;
                        const isAtMergeBase = this.isTemplateEqual(template, mergeBaseTemplate);

                        if ( isAtMergeBase ) {
                            return {
                                historyPast: lastHistoryTemplate && this.isTemplateEqual(lastHistoryTemplate, mergeBaseTemplate)
                                    ? prevState.historyPast.slice(0, -1)
                                    : prevState.historyPast,
                                historyFuture: [],
                                historyPastCapped: prevState.historyPastCapped,
                            };
                        }

                        const trimmedHistoryPast = this.trimHistoryPast(prevState.historyPast.concat([ this.cloneTemplate(mergeBaseTemplate) ]), prevState.historyPastCapped);

                        return {
                            historyPast: lastHistoryTemplate && this.isTemplateEqual(lastHistoryTemplate, mergeBaseTemplate)
                                ? prevState.historyPast
                                : trimmedHistoryPast.historyPast,
                            historyFuture: [],
                            historyPastCapped: lastHistoryTemplate && this.isTemplateEqual(lastHistoryTemplate, mergeBaseTemplate)
                                ? prevState.historyPastCapped
                                : trimmedHistoryPast.historyPastCapped,
                        };
                    })()
                    : this.recordHistoryBeforeChange(prevState, { mode: historyMode }) ),
                template,
            };
        }, () => {
            if ( historyBoundary ) {
                this.closeHistoryDebounceWindow();
            }
        });
    }

    onChangeHTML(html, options = {}) {
        if ( ! this.state.template ) {
            return;
        }

        const resetHistory = !! options.resetHistory;
        if ( resetHistory ) {
            this.closeHistoryDebounceWindow();
            this.resetLastTemplatePropertyChange();

            this.setState((prevState) => {
                if ( ! prevState.template ) {
                    return null;
                }

                const noHistoryToReset = 0 === prevState.historyPast.length && 0 === prevState.historyFuture.length;
                if ( html === prevState.template.html && noHistoryToReset ) {
                    return null;
                }

                const template = this.cloneTemplate(prevState.template);
                template.html = html;

                return {
                    template,
                    historyPast: [],
                    historyFuture: [],
                    historyPastCapped: false,
                    codeHistoryResetNotice: this.historyEnabled,
                };
            });
            return;
        }

        const historyMode = options.historyMode || 'debounced';
        const historyBoundary = !! options.historyBoundary;
        const historyMergeKey = options.historyPropertyId ? `html:${options.historyPropertyId}` : false;
        const now = Date.now();
        const shouldMergeChange = this.historyEnabled && historyMergeKey && this.shouldMergeWithPreviousTemplatePropertyChange(historyMergeKey, now);
        const mergeBaseTemplate = shouldMergeChange && this.lastTemplatePropertyChange.baseTemplate
            ? this.cloneTemplate(this.lastTemplatePropertyChange.baseTemplate)
            : this.cloneTemplate(this.state.template);

        if ( html === this.state.template.html ) {
            if ( historyBoundary ) {
                this.closeHistoryDebounceWindow();
            }
            return;
        }

        if ( this.historyEnabled && historyMergeKey ) {
            this.lastTemplatePropertyChange = {
                id: historyMergeKey,
                at: now,
                baseTemplate: mergeBaseTemplate,
            };
        } else {
            this.resetLastTemplatePropertyChange();
        }

        this.setState((prevState) => {
            if ( ! prevState.template || html === prevState.template.html ) {
                return null;
            }

            const template = this.cloneTemplate(prevState.template);
            template.html = html;

            return {
                ...( shouldMergeChange
                    ? (() => {
                        const lastHistoryTemplate = prevState.historyPast.length ? prevState.historyPast[ prevState.historyPast.length - 1 ] : false;
                        const isAtMergeBase = this.isTemplateEqual(template, mergeBaseTemplate);

                        if ( isAtMergeBase ) {
                            return {
                                historyPast: lastHistoryTemplate && this.isTemplateEqual(lastHistoryTemplate, mergeBaseTemplate)
                                    ? prevState.historyPast.slice(0, -1)
                                    : prevState.historyPast,
                                historyFuture: [],
                                historyPastCapped: prevState.historyPastCapped,
                            };
                        }

                        const trimmedHistoryPast = this.trimHistoryPast(prevState.historyPast.concat([ this.cloneTemplate(mergeBaseTemplate) ]), prevState.historyPastCapped);

                        return {
                            historyPast: lastHistoryTemplate && this.isTemplateEqual(lastHistoryTemplate, mergeBaseTemplate)
                                ? prevState.historyPast
                                : trimmedHistoryPast.historyPast,
                            historyFuture: [],
                            historyPastCapped: lastHistoryTemplate && this.isTemplateEqual(lastHistoryTemplate, mergeBaseTemplate)
                                ? prevState.historyPastCapped
                                : trimmedHistoryPast.historyPastCapped,
                        };
                    })()
                    : this.recordHistoryBeforeChange(prevState, { mode: historyMode }) ),
                template,
            };
        }, () => {
            if ( historyBoundary ) {
                this.closeHistoryDebounceWindow();
            }
        });
    }

    onChangeCSS(css, options = {}) {
        if ( ! this.state.template || ! this.state.template.style ) {
            return;
        }

        const resetHistory = !! options.resetHistory;
        if ( resetHistory ) {
            this.closeHistoryDebounceWindow();
            this.resetLastTemplatePropertyChange();

            this.setState((prevState) => {
                if ( ! prevState.template || ! prevState.template.style ) {
                    return null;
                }

                const noHistoryToReset = 0 === prevState.historyPast.length && 0 === prevState.historyFuture.length;
                if ( css === prevState.template.style.css && noHistoryToReset ) {
                    return null;
                }

                const template = this.cloneTemplate(prevState.template);
                template.style.css = css;

                return {
                    template,
                    historyPast: [],
                    historyFuture: [],
                    historyPastCapped: false,
                    codeHistoryResetNotice: this.historyEnabled,
                };
            });
            return;
        }

        this.resetLastTemplatePropertyChange();

        const historyMode = options.historyMode || 'debounced';
        const historyBoundary = !! options.historyBoundary;

        if ( css === this.state.template.style.css ) {
            if ( historyBoundary ) {
                this.closeHistoryDebounceWindow();
            }
            return;
        }

        this.setState((prevState) => {
            if ( ! prevState.template || ! prevState.template.style || css === prevState.template.style.css ) {
                return null;
            }

            const template = this.cloneTemplate(prevState.template);
            template.style.css = css;

            return {
                ...this.recordHistoryBeforeChange(prevState, { mode: historyMode }),
                template,
            };
        }, () => {
            if ( historyBoundary ) {
                this.closeHistoryDebounceWindow();
            }
        });
    }

    onChangeShortcode(shortcode) {
        if ( shortcode !== this.state.shortcode ) {
            // Update URL when shortcode changes
            if (shortcode && shortcode.id) {
                this.updateUrl(`/shortcode/${shortcode.id}`);
            } else {
                this.updateUrl('/shortcode');
            }
            
            this.setState({
                shortcode
            });
        }
    }

    onDeleteTemplate(slug) {
        if ( ! this.state.savingTemplate ) {
            this.setState({
                savingTemplate: true,
            });
    
            Api.template.delete(slug).then(deletedSlug => {
                if ( deletedSlug ) {
                    let newState = this.state;

                    newState.savingTemplate = false;
                    newState.template = false;
                    delete newState.templates[deletedSlug];
    
                    // If we were editing the deleted template, go back to manage
                    if (newState.editing && newState.template && newState.template.slug === deletedSlug) {
                        newState.editing = false;
                        newState.mode = 'manage';
                        this.updateManageUrl(newState.manageTemplateType, false);
                    } else if (newState.mode === 'manage' && newState.template && newState.template.slug === deletedSlug) {
                        // If we were viewing the deleted template in manage mode, clear selection
                        newState.template = false;
                        this.updateManageUrl(newState.manageTemplateType, false);
                    }
    
                    this.setState(newState);
                } else {
                    this.setState({
                        savingTemplate: false,
                    });
                }
            });
        }
    }

    onSaveTemplate(template) {
        if ( ! this.state.savingTemplate ) {
            this.setState({
                savingTemplate: true,
            });

            // For new templates without a style property, use the raw CSS
            // For existing templates, parse the CSS to handle style properties
            const parsedTemplate = {
                ...template,
                css: template.style && template.style.css !== undefined 
                    ? Helpers.parseCSS(template)
                    : (template.css || ''),
            }
    
            Api.template.save(parsedTemplate).then(savedTemplate => {
                if ( savedTemplate ) {
                    const slug = savedTemplate.slug;
                    const newTemplates = { ...this.state.templates };
                    if ( slug ) {
                        newTemplates[slug] = savedTemplate;
                    }

                    this.setState({
                        savingTemplate: false,
                        templates: newTemplates,
                    }, () => {
                        // Force refresh of active template to make sure things are synced.
                        // If we're editing, update URL to the new slug with current mode
                        if (this.state.editing && slug) {
                            const mode = this.state.mode || 'properties';
                            // Allow editingBlock to be 0 (first block) - only exclude false
                            const editingBlock = (this.state.mode === 'blocks' && this.state.editingBlock !== false && typeof this.state.editingBlock === 'number') ? this.state.editingBlock : false;
                            if (editingBlock !== false) {
                                this.updateUrl(`/template/${slug}/${mode}/${editingBlock}`);
                            } else {
                                this.updateUrl(`/template/${slug}/${mode}`);
                            }
                        }
                        
                        // Make sure template exists in state before trying to change to it
                        if (this.state.templates.hasOwnProperty(slug)) {
                            this.onChangeTemplate(slug);
                            
                            // Automatically open for editing if this is a blank template (empty html and css)
                            const isBlankTemplate = savedTemplate && 
                                (!savedTemplate.html || savedTemplate.html.trim() === '') && 
                                (!savedTemplate.css || savedTemplate.css.trim() === '');
                            if (isBlankTemplate) {
                                // Use setTimeout to ensure template is loaded before opening for editing
                                setTimeout(() => {
                                    this.onChangeEditing(true);
                                }, 100);
                            }
                        } else {
                            console.error('Template not found in state after saving:', slug, this.state.templates);
                        }
                    });
                } else {
                    this.setState({
                        savingTemplate: false,
                    });
                    alert('Failed to save template. The server did not return a valid template. Please try again.');
                }
            }).catch(error => {
                console.error('Error saving template:', error);
                this.setState({
                    savingTemplate: false,
                });
                alert('An error occurred while saving the template. Please check the console for details.');
            });
        }
    }

    render() {
        return (
            <div>
                <Menu
                    mode={ this.state.mode }
                    editing={ this.state.editing }
                    changesMade={ this.changesMade() }
                    onChangeEditing={ this.onChangeEditing.bind(this) }
                    savingTemplate={ this.state.savingTemplate }
                    onSaveTemplate={ this.onSaveTemplate.bind(this) }
                    onChangeMode={ this.onChangeMode.bind(this) }
                    templates={ this.state.templates }
                    template={ this.state.template }
                    onChangeTemplate={ this.onChangeTemplate.bind(this) }
                    onChangeTemplateProperty={ this.onChangeTemplateProperty.bind(this) }
                    fonts={ this.state.template && this.state.template.fonts ? this.state.template.fonts : [] }
                    onChangeFonts={ this.onChangeFonts.bind(this) }
                    sidebarCollapsed={ this.state.sidebarCollapsed }
                    onToggleSidebar={ this.onToggleSidebar.bind(this) }
                    historyEnabled={ this.historyEnabled }
                    undoAtMax={ this.state.historyPastCapped && this.state.historyPast.length >= this.historyMaxSteps }
                    canUndo={ this.state.historyPast.length > 0 }
                    canRedo={ this.state.historyFuture.length > 0 }
                    undoCount={ this.state.historyPast.length }
                    redoCount={ this.state.historyFuture.length }
                    onUndo={ this.onUndo.bind(this) }
                    onRedo={ this.onRedo.bind(this) }
                    codeHistoryResetNotice={ this.state.codeHistoryResetNotice }
                />
                <Main
                    mode={ this.state.mode }
                    onChangeMode={ this.onChangeMode.bind(this) }
                    editing={ this.state.editing }
                    onChangeEditing={ this.onChangeEditing.bind(this) }
                    savingTemplate={ this.state.savingTemplate }
                    onDeleteTemplate={ this.onDeleteTemplate.bind(this) }
                    onSaveTemplate={ this.onSaveTemplate.bind(this) }
                    templates={ this.state.templates }
                    template={ this.state.template }
                    onChangeTemplate={ this.onChangeTemplate.bind(this) }
                    onChangeHTML={ this.onChangeHTML.bind(this) }
                    onChangeCSS={ this.onChangeCSS.bind(this) }
                    shortcode={ this.state.shortcode }
                    onChangeShortcode={ this.onChangeShortcode.bind(this) }
                    editingBlock={ this.state.editingBlock }
                    onChangeEditingBlock={ this.onChangeEditingBlock.bind(this) }
                    manageTemplateType={ this.state.manageTemplateType }
                    onChangeManageTemplateType={ this.onChangeManageTemplateType.bind(this) }
                    defaultTemplateUsages={ this.state.defaultTemplateUsages }
                />
            </div>
        );
    }
}

export default withRouter(App);