<?php

namespace GriffinForms\Admin\App\Html\Widgets\PresentationArea;

class FormLayout extends \GriffinForms\Admin\App\Html\Widgets\PresentationArea\Format
{
    protected $form;
    protected $item_id;

    public function getHtml()
    {
        echo '<div id="' . esc_attr($this->getHtmlId()) . '" data-gf-context="form-layout" class="">';
        echo '</div>';
    }

    public function getFormLayoutHtml()
    {
        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'form_layout') : false;
        
        if ($nonce_status === false) {
            $this->nonceError();
        }

        if (!isset($_POST['form_id'])) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Form ID is missing.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $this->item_id = absint(wp_unslash($_POST['form_id']));
        $this->formLayoutHtml();

        wp_die();
    }

    private function formLayoutHtml()
    {
        if ($this->sql->itemExists('form', $this->item_id) === false) {
            $this->missingItemError('form');
            return;
        }

        $form_theme_fonts = array();
        $form_theme_id = $this->sql->getItemValue($this->item_id, 'form_theme', 'form');
        if ($form_theme_id) {
            $form_theme = new \GriffinForms\Items\FormTheme();
            if ($form_theme->loadItem(absint($form_theme_id))) {
                $fonts = $form_theme->getExternalFontFamilies();
                if (is_array($fonts)) {
                    $form_theme_fonts = array_values(array_filter(array_map('trim', $fonts)));
                }
            }
        }

        if ($this->isAiDraftMode()) {
            $this->renderAiDraftBanner();
        }

        $form_html = new FormElements\Form($this->item_id);
        $form_html->getHtml();

        // Print initial font list for JS to consume
        echo '<script>window.griffinformsInitialThemeFonts = ' . wp_json_encode($form_theme_fonts) . ';</script>';
        // Set a global flag for theme presence (1 = themed, 0 = no theme)
        echo '<script>window.griffinformsIsThemeSet = ' . ( $form_theme_id ? 1 : 0 ) . ';</script>';
    }

    private function isAiDraftMode(): bool
    {
        $mode = isset($_POST['mode']) ? sanitize_key(wp_unslash($_POST['mode'])) : '';
        return $mode === 'ai_draft' && $this->item_id > 0;
    }

    private function isAiDraftForm(int $form_id): bool
    {
        if ($form_id <= 0 || $this->sql->itemExists('form', $form_id) === false) {
            return false;
        }

        if ($this->formTableHasColumn('status')) {
            $status = (string) $this->sql->getItemValue($form_id, 'status', 'form');
            if ($status === 'draft') {
                return true;
            }
        }

        $origin = (string) $this->sql->getItemValue($form_id, 'origin', 'form');
        if ($origin === 'ai') {
            return true;
        }

        $integration_settings = $this->sql->getItemValue($form_id, 'integration_settings', 'form');
        $settings = $this->maybeDecode($integration_settings);

        return is_array($settings) && is_array($settings['ai_draft'] ?? null);
    }

    private function formTableHasColumn(string $column): bool
    {
        global $wpdb;

        $table = $this->config->getTable('form');
        $exists = $wpdb->get_var($wpdb->prepare(
            "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = %s AND COLUMN_NAME = %s",
            $table,
            $column
        ));

        return !empty($exists);
    }

    private function renderAiDraftBanner(): void
    {
        $publish_nonce = wp_create_nonce('publish_ai_draft');
        $discard_nonce = wp_create_nonce('discard_ai_draft');

        echo '<div class="gf-ai-draft-banner my-2" data-gf-ai-draft-banner="true" data-gf-form-id="' . esc_attr($this->item_id) . '" data-gf-publish-nonce="' . esc_attr($publish_nonce) . '" data-gf-discard-nonce="' . esc_attr($discard_nonce) . '">';
        echo '<div class="gf-ai-draft-banner__copy">';
        echo '<span class="gf-ai-draft-banner__eyebrow">' . esc_html__('Griffin Assist Draft') . '</span>';
        echo '<span class="gf-ai-draft-banner__text">' . esc_html__('Finalize Draft keeps this draft and leaves the builder in its current state. Discard permanently deletes it.', 'griffinforms-form-builder') . '</span>';
        echo '</div>';
        echo '<div class="gf-ai-draft-banner__actions">';
        echo '<button type="button" class="btn btn-primary gf-ai-draft-banner__publish" data-gf-ai-draft-action="publish">' . esc_html__('Finalize Draft', 'griffinforms-form-builder') . '</button>';
        echo '<button type="button" class="btn btn-outline-danger gf-ai-draft-banner__discard" data-gf-ai-draft-action="discard">' . esc_html__('Discard Draft', 'griffinforms-form-builder') . '</button>';
        echo '</div>';
        echo '</div>';
    }

    public function updateItemName()
    {
        $response = array(
            'time' => wp_date(get_option('time_format'))
        );

        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'update_item_name') : false;

        if ($nonce_status === false) {
            $response["success"] = false;
            $response["message"] = __('Nonce error', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $item_type = isset($_POST['item_type']) ? sanitize_text_field(wp_unslash($_POST['item_type'])) : '';
        $item_id = isset($_POST['item_id']) ? sanitize_text_field(wp_unslash($_POST['item_id'])) : 0;
        $field_value = isset($_POST['field_value']) ? sanitize_text_field(wp_unslash($_POST['field_value'])) : '';

        if ($field_value === '') {
            $response["success"] = false;
            $response["message"] = __('Cannot save empty name', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $updated = $this->sql->updateItemValue($item_type, $item_id, 'name', $field_value);

        if ($updated) {
            $response["success"] = true;
            $response["updated_value"] = $field_value;
            // Translators: %1$s refers to the type of item, %2$d refers to the item ID
            $response["message"] = sprintf(__('%1$s (ID %2$d) name successfully updated.', 'griffinforms-form-builder'), ucfirst($item_type), $item_id);

            $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;
            if ($form_id > 0) {
                $this->refreshFormLayoutMetadata($form_id, 'builder_inline_edit', array(
                    'field' => 'name',
                    'item_type' => $item_type,
                    'item_id' => $item_id
                ));
            }
        } else {
            $response["success"] = false;
            // Translators: %s refers to the type of item
            $response["message"] = sprintf(__('%s name was not updated.', 'griffinforms-form-builder'), ucfirst($item_type));
        }

        echo wp_json_encode($response);
        wp_die();
    }

    public function updateItemHeading()
    {
        $response = array(
            'time' => wp_date(get_option('time_format'))
        );

        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'update_item_heading') : false;

        if ($nonce_status === false) {
            $response["success"] = false;
            $response["message"] = __('Nonce error', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $item_type = isset($_POST['item_type']) ? sanitize_text_field(wp_unslash($_POST['item_type'])) : '';
        $item_id = isset($_POST['item_id']) ? sanitize_text_field(wp_unslash($_POST['item_id'])) : 0;
        $field_value = isset($_POST['field_value']) ? sanitize_text_field(wp_unslash($_POST['field_value'])) : '';

        $updated = $this->sql->updateItemValue($item_type, $item_id, 'heading', $field_value);

        if ($updated) {
            $response["success"] = true;
            $response["updated_value"] = $field_value;
            // Translators: %1$s refers to the type of item, %2$d refers to the item ID
            $response["message"] = sprintf(__('%1$s (ID %2$d) label successfully updated.', 'griffinforms-form-builder'), ucfirst($item_type), $item_id);

            $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;
            if ($form_id > 0) {
                $this->refreshFormLayoutMetadata($form_id, 'builder_inline_edit', array(
                    'field' => 'heading',
                    'item_type' => $item_type,
                    'item_id' => $item_id
                ));
            }
        } else {
            $response["success"] = false;
            // Translators: %s refers to the type of item
            $response["message"] = sprintf(__('%s heading was not updated.', 'griffinforms-form-builder'), ucfirst($item_type));
        }

        echo wp_json_encode($response);
        wp_die();
    }

    public function updateItemDescription()
    {
        $response = array(
            'time' => wp_date(get_option('time_format'))
        );

        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'update_item_description') : false;

        if ($nonce_status === false) {
            $response["success"] = false;
            $response["message"] = __('Nonce error', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $item_type = isset($_POST['item_type']) ? sanitize_text_field(wp_unslash($_POST['item_type'])) : '';
        $item_id = isset($_POST['item_id']) ? sanitize_text_field(wp_unslash($_POST['item_id'])) : 0;
        $field_value = isset($_POST['field_value']) ? sanitize_textarea_field(wp_unslash($_POST['field_value'])) : '';

        if (strlen($field_value) > 2000) {
            $field_value = substr($field_value, 0, 2000);
        }

        $updated = $this->sql->updateItemValue($item_type, $item_id, 'description', $field_value);
        
        if ($updated) {
            $response["success"] = true;
            $response["updated_value"] = $field_value;
            // Translators: %1$s refers to the name of the item type, %2$d refers to the item ID
            $response["message"] = sprintf(__('%1$s (ID %2$d) description successfully updated.', 'griffinforms-form-builder'), $this->config->getFriendlyName($item_type), $item_id);

            $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;
            if ($form_id > 0) {
                $this->refreshFormLayoutMetadata($form_id, 'builder_inline_edit', array(
                    'field' => 'description',
                    'item_type' => $item_type,
                    'item_id' => $item_id
                ));
            }
        } else {
            $response["success"] = false;
            // translators: %s refers to the name of the item type
            $response["message"] = sprintf(__('%s description was not updated.', 'griffinforms-form-builder'), $this->config->getFriendlyName($item_type));
        }

        echo wp_json_encode($response);
        wp_die();
    }

    public function updatePageStepData()
    {
        $response = array(
            'time' => wp_date(get_option('time_format'))
        );

        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'update_page_step_data') : false;

        if ($nonce_status === false) {
            $response['success'] = false;
            $response['message'] = __('Nonce error', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $page_id = isset($_POST['page_id']) ? absint(wp_unslash($_POST['page_id'])) : 0;
        $step_name = isset($_POST['step_name']) ? sanitize_text_field(wp_unslash($_POST['step_name'])) : '';
        $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;

        if ($page_id < 1 || !$this->sql->itemExists('formpage', $page_id)) {
            $response['success'] = false;
            $response['message'] = __('Page not found.', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $current_step_data = $this->sql->getItemValue($page_id, 'step_data', 'formpage');
        $current_step_data = is_string($current_step_data) ? maybe_unserialize($current_step_data) : $current_step_data;
        if (!is_array($current_step_data)) {
            $current_step_data = array();
        }

        $current_step_data['step_name'] = $step_name;
        if (!isset($current_step_data['step_icon'])) {
            $current_step_data['step_icon'] = '';
        }

        $updated = $this->sql->updateItemValue('formpage', $page_id, 'step_data', maybe_serialize($current_step_data));

        if ($updated !== false) {
            $response['success'] = true;
            $response['updated_value'] = array(
                'step_name' => $step_name,
                'step_icon' => sanitize_text_field($current_step_data['step_icon'] ?? ''),
            );
            $response['message'] = sprintf(__('Form page (ID %d) step label successfully updated.', 'griffinforms-form-builder'), $page_id);

            if ($form_id > 0) {
                $this->refreshFormLayoutMetadata($form_id, 'builder_inline_edit', array(
                    'field' => 'step_name',
                    'item_type' => 'formpage',
                    'item_id' => $page_id,
                ));
            }
        } else {
            $response['success'] = false;
            $response['message'] = __('Step label was not updated.', 'griffinforms-form-builder');
        }

        echo wp_json_encode($response);
        wp_die();
    }

        /**
     * Creates a new form element item when called via AJAX.
     * @return mixed newly created item's id or false on failure.
     * @package new-builder
     */
    public function createFormElement()
    {
        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'create_formelement') : false;
        $response = array();
        $item_type = isset($_POST['item_type']) ? sanitize_text_field(wp_unslash($_POST['item_type'])) : '';

        if ($nonce_status === false) {
            $response['success'] = false;
            $response['type'] = 'failure';
            $response['msg'] = __('Nonce error', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $item_id = false;
        $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;
        $sql_method = 'create' . $item_type;

        if (method_exists($this->sql, $sql_method)) {
            if ($item_type === 'rowcolumn') {
                $item_id = $this->sql->createRowColumn(12, $form_id);
            } else {
                $item_id = $this->sql->$sql_method($form_id);
            }
        }

        if ($item_id === false) {
            $response['type'] = 'failure';
            $response['msg'] = __('Unable to create item', 'griffinforms-form-builder');
        } else {
            $response['type'] = 'success';
            $response['item_id'] = $item_id;
            $response['msg'] = $this->getInsertedItemStatusMessage($item_type, 'create');
        }

        echo wp_json_encode($response);
        wp_die();

    }

    /**
     * Attaches a child formelement to parent when called via AJAX.
     * @see createFormElement which usually is called before this function to generate new element and pass its ID.
     * @package new-builder
     */
    public function attachFormElement()
    {
        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'attach_formelement') : false;
        $response = array();

        if ($nonce_status === false) {
            $response['type'] = 'failure';
            $response['msg'] = __('Nonce error. Unable to attach item.', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $parent_type = isset($_POST['parent_type']) ? sanitize_text_field(wp_unslash($_POST['parent_type'])) : '';
        $item_type = isset($_POST['item_type']) ? sanitize_text_field(wp_unslash($_POST['item_type'])) : '';
        $parent_id = isset($_POST['parent_id']) ? absint(wp_unslash($_POST['parent_id'])) : 0;
        $item_id = isset($_POST['item_id']) ? absint(wp_unslash($_POST['item_id'])) : 0;
        $parent_type_raw = $parent_type;

        $current_items = $this->maybeDecode($this->sql->getItemValue($parent_id, $item_type, $parent_type)); // Gets list of current child elements.

        if (empty($current_items) or !is_array($current_items)) {
            $updated_items = array($item_id); // If parent is empty we just need to insert child id.
        } else {
            // If parent is not empty we need to find a position to insert new child element.

            // If ID of previous item is not provided, we assume it will be appended in the end. Therefore previous item ID will be last element of $current_items.

            if (empty($_POST['prev_item_id']) or $_POST['prev_item_id'] === null) {
                // If ID of previous item is not provided, we assume it will be appended in the end. Therefore previous item ID will be last element of $current_items.
                $prev_item_id = $current_items[array_key_last($current_items)];
            } else {
                // Contains ID of the child element after which the new element will be inserted.
                $prev_item_id = absint(wp_unslash($_POST['prev_item_id']));
            }

            $index = array_search($prev_item_id, $current_items); // Find index value of the previous child element.

            // To insert ID of new child element we need to split the array of existing child elements and then add new element in between.
            $prev_items = array_slice($current_items, 0 , $index + 1);
            $inserted_item = array($item_id);
            $next_items = array_slice($current_items, $index + 1);

            $updated_items = array_merge($prev_items, $inserted_item, $next_items); // The updated array of child elements.

        }

        // Updated child items are ready to be inserted into database table.
        global $wpdb;
        $table = $this->config->getTable($parent_type);
        $data = array($item_type => maybe_serialize($updated_items));
        $where = array('id' => $parent_id);

        $update = $wpdb->update($table, $data, $where, array('%s'), array('%d'));
        $parent_type = $this->config->getFriendlyName($parent_type);

        if (is_wp_error($update) or $update === false) { // When updating row fails.
            $response['type'] = 'failure';
            $response['msg'] = __('There was a problem updating table.', 'griffinforms-form-builder');
        }

        if ($update === 0) { // When the new data is same as existing data and row is not updated.
            $response['type'] = 'failure';
            $response['msg'] = __('There was nothing to update.', 'griffinforms-form-builder');
        }

        if ($update === 1) { // When row is updated.
            $starter_row_id = 0;

            // Issue 3: enforce page -> starter row server-side to avoid UI-only drift.
            if ($parent_type_raw === 'form' && $item_type === 'formpages') {
                $target_form_id = $this->resolveFormIdFromRequest([
                    'parent_type' => $parent_type_raw,
                    'parent_id' => $parent_id
                ]);

                if ($target_form_id <= 0) {
                    $target_form_id = $parent_id;
                }

                $starter_row_id = (int) $this->sql->createPageRow($target_form_id);
                if ($starter_row_id > 0) {
                    $this->sql->updateFormPageRows($item_id, array($starter_row_id));
                } else if (!empty($this->log)) {
                    $this->log->add(
                        'warning',
                        'formpage',
                        (int) $item_id,
                        get_current_user_id(),
                        'Unable to auto-create starter row while attaching new page.',
                        'builder'
                    );
                }
            }

            $response['type'] = 'success';
            $response['msg'] = $this->getInsertedItemStatusMessage($item_type, 'attach');
            if ($starter_row_id > 0) {
                $response['starter_row_id'] = $starter_row_id;
            }
            if ($item_type === 'formpages' || $item_type === 'pagerows') {
                $render_type = $item_type === 'formpages' ? 'formpage' : 'pagerow';
                $render_html = $this->renderFormElementHtmlFragment(
                    $render_type,
                    $item_id,
                    $parent_id,
                    $this->resolveFormIdFromRequest([
                        'parent_type' => $parent_type_raw,
                        'parent_id' => $parent_id
                    ])
                );
                if ($render_html !== '') {
                    $response['html'] = $render_html;
                }
            }
            $this->refreshFormLayoutMetadata(
                $this->resolveFormIdFromRequest([
                    'parent_type' => $parent_type_raw,
                    'parent_id' => $parent_id
                ]),
                'builder_attach_item',
                array(
                    'parent_type' => $parent_type_raw,
                    'parent_id' => $parent_id,
                    'item_type' => $item_type,
                    'item_id' => $item_id
                )
            );
        }

        echo wp_json_encode($response);
        wp_die();

    }

    public function getFormElementHtml() {
        $item_type = isset($_POST['item_type']) ? sanitize_text_field(wp_unslash($_POST['item_type'])) : '';
        $item_id = isset($_POST['item_id']) ? absint(wp_unslash($_POST['item_id'])) : 0;
        $parent_id = isset($_POST['parent_id']) ? absint(wp_unslash($_POST['parent_id'])) : 0;
        $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;

        $html = $this->renderFormElementHtmlFragment($item_type, $item_id, $parent_id, $form_id);
        if ($html === '') {
            esc_html_e('Required POST data missing.', 'griffinforms-form-builder');
            wp_die();
        }

        echo $html;
        
        wp_die();
    }

    public function createField()
    {
        $this->checkNonce('create_field');
        $this->checkCapabilities();

        $field_type = isset($_POST['field_type']) ? sanitize_text_field(wp_unslash($_POST['field_type'])) : '';

        // Validate the field type before proceeding
        if (!$this->config->isValidFieldType($field_type)) {
            $this->response['type'] = 'danger';
            $this->response['success'] = false;
            $this->response['msg'] = __('Invalid field type.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $insert = $this->sql->createField(array('field_type' => $field_type, 'is_reusable' => 0));
        if ($insert === false) {
            global $wpdb;
            $this->response['type'] = 'danger';
            $this->response['success'] = false;
            $this->response['msg'] = $wpdb->last_error;
        } else {
            $this->response['type'] = 'success';
            $this->response['success'] = true;
            $this->response['msg'] = $this->getInsertedItemStatusMessage('field', 'create');
            $this->response['field_id'] = $insert;
        }

        echo wp_json_encode($this->response);
        wp_die();
    }

    public function updateRowColumnFields()
    {
        $this->checkNonce('update_rowcolumn_fields');
        $this->checkCapabilities();

        if (!isset($_POST['column_id'])) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Column ID is missing.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }
        
        if (!isset($_POST['column_id'])) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Column ID is missing.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $column_id = absint(wp_unslash($_POST['column_id']));

        // For some reason, AJAX removes the key field_ids when column is empty. A temporary fix.
        if (!isset($_POST['field_ids']) || !is_array($_POST['field_ids'])) {
            $field_ids = array();
        } else {
            $field_ids = array_map('absint', wp_unslash($_POST['field_ids']));
        }

        $render_field_id = isset($_POST['render_field_id']) ? absint(wp_unslash($_POST['render_field_id'])) : 0;

        $update = $this->sql->updateRowColumnFields($column_id, $field_ids);

        if ($update === 0) {
            global $wpdb;
            $this->response['success'] = false;
            $this->response['msg'] = $wpdb->last_error;
        } else {
            $this->response['success'] = true;
            $this->response['msg'] = $render_field_id > 0
                ? $this->getInsertedItemStatusMessage('field', 'attach')
                : __('Field order updated successfully.', 'griffinforms-form-builder');
            if ($render_field_id > 0) {
                $render_html = $this->renderFormElementHtmlFragment(
                    'field',
                    $render_field_id,
                    $column_id,
                    $this->resolveFormIdFromRequest(['column_id' => $column_id])
                );
                if ($render_html !== '') {
                    $this->response['html'] = $render_html;
                }
            }
            $this->refreshFormLayoutMetadata(
                $this->resolveFormIdFromRequest(['column_id' => $column_id]),
                'builder_field_order',
                array('column_id' => $column_id)
            );
        }

        echo wp_json_encode($this->response);
        wp_die();
    }

    protected function renderFormElementHtmlFragment(string $item_type, int $item_id, int $parent_id, int $form_id): string
    {
        $item_type = sanitize_text_field($item_type);
        $item_class = $this->resolveFormElementClassName($item_type);
        $parent_class = $this->resolveFormElementParentClassName($item_type);

        if ($item_type === '' || $item_id < 1 || $parent_id < 0 || $form_id < 1 || $item_class === '') {
            return '';
        }

        $form_theme_id = $this->sql->getItemValue($form_id, 'form_theme', 'form');
        $form_theme = new \GriffinForms\Items\FormTheme();
        $form_theme->loadItem($form_theme_id);
        $parent = false;

        if ($parent_class !== '') {
            $parent = new $parent_class($parent_id, 0, false, $form_theme);
        }

        ob_start();
        $item = new $item_class($item_id, $parent_id, $parent, $form_theme);
        $item->getHtml();
        return (string) ob_get_clean();
    }

    protected function resolveFormElementClassName(string $item_type): string
    {
        $item_type = strtolower(trim($item_type));
        $map = array(
            'formpage' => 'GriffinForms\Admin\App\Html\Widgets\PresentationArea\FormElements\FormPage',
            'pagerow' => 'GriffinForms\Admin\App\Html\Widgets\PresentationArea\FormElements\PageRow',
            'rowcolumn' => 'GriffinForms\Admin\App\Html\Widgets\PresentationArea\FormElements\RowColumn',
            'field' => 'GriffinForms\Admin\App\Html\Widgets\PresentationArea\FormElements\Field',
        );

        return $map[$item_type] ?? '';
    }

    protected function resolveFormElementParentClassName(string $item_type): string
    {
        $item_type = strtolower(trim($item_type));
        $map = array(
            'formpage' => 'GriffinForms\Admin\App\Html\Widgets\PresentationArea\FormElements\Form',
            'pagerow' => 'GriffinForms\Admin\App\Html\Widgets\PresentationArea\FormElements\FormPage',
            'rowcolumn' => 'GriffinForms\Admin\App\Html\Widgets\PresentationArea\FormElements\PageRow',
            'field' => 'GriffinForms\Admin\App\Html\Widgets\PresentationArea\FormElements\RowColumn',
        );

        return $map[$item_type] ?? '';
    }

    protected function getInsertedItemStatusMessage(string $item_type, string $stage = 'create'): string
    {
        $item_type = strtolower(trim($item_type));
        $stage = strtolower(trim($stage));

        $label_map = array(
            'formpage' => __('Page', 'griffinforms-form-builder'),
            'formpages' => __('Page', 'griffinforms-form-builder'),
            'pagerow' => __('Row', 'griffinforms-form-builder'),
            'pagerows' => __('Row', 'griffinforms-form-builder'),
            'rowcolumn' => __('Column', 'griffinforms-form-builder'),
            'field' => __('Field', 'griffinforms-form-builder'),
        );

        $label = $label_map[$item_type] ?? __('Item', 'griffinforms-form-builder');

        if ($stage === 'attach') {
            return sprintf(__('%s inserted successfully.', 'griffinforms-form-builder'), $label);
        }

        return sprintf(__('%s created successfully.', 'griffinforms-form-builder'), $label);
    }

    public function updateFormPageRows()
    {
        $this->checkNonce('update_formpage_rows');
        $this->checkCapabilities();

        if (!isset($_POST['page_id'])) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Page ID is missing.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $page_id = absint(wp_unslash($_POST['page_id']));

        if (!isset($_POST['row_ids']) || !is_array($_POST['row_ids'])) {
            $row_ids = array();
        } else {
            $row_ids = array_map('absint', wp_unslash($_POST['row_ids']));
        }

        $update = $this->sql->updateFormPageRows($page_id, $row_ids);

        if ($update === 0) {
            global $wpdb;
            $this->response['success'] = false;
            $this->response['msg'] = $wpdb->last_error;
        } else {
            $this->response['success'] = true;
            $this->response['msg'] = __('Row order updateded successfully.', 'griffinforms-form-builder');
            $this->refreshFormLayoutMetadata(
                $this->resolveFormIdFromRequest(['page_id' => $page_id]),
                'builder_row_order',
                array('page_id' => $page_id)
            );
        }

        echo wp_json_encode($this->response);
        
        wp_die();
    }

    public function createRowColumn()
    {
        $this->checkNonce('create_rowcolumn');
        $this->checkCapabilities();

        if (!isset($_POST['column_width'])) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Column width is missing.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $column_width = absint(wp_unslash($_POST['column_width']));
        $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;

        $result = $this->sql->createRowColumn($column_width, $form_id);

        if (is_int($result) and $result > 0) {
            $this->response['success'] = true;
            $this->response['column_id'] = $result;
            $this->response['msg'] = $this->getInsertedItemStatusMessage('rowcolumn', 'create');
        } else {
            $this->response['success'] = false;
            $this->response['msg'] = __('Unable to create new column.', 'griffinforms-form-builder');
        }

        echo wp_json_encode($this->response);
        wp_die();
    }

    public function attachRowColumn()
    {
        $this->checkNonce('attach_rowcolumn');
        $this->checkCapabilities();
        
        if (!isset($_POST['row_id'])) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Row ID is missing.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        if (!isset($_POST['column_id'])) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Column ID is missing.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        $row_id = absint(wp_unslash($_POST['row_id']));
        $column_id = absint(wp_unslash($_POST['column_id']));

        $insert = $this->sql->attachRowColumn($row_id, $column_id);

        if ($insert === false) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Unable to attach the new column to the row.', 'griffinforms-form-builder');
        } else {
            $this->response['success'] = true;
            $this->response['msg'] = $this->getInsertedItemStatusMessage('rowcolumn', 'attach');
            $render_html = $this->renderFormElementHtmlFragment(
                'rowcolumn',
                $column_id,
                $row_id,
                $this->resolveFormIdFromRequest(['row_id' => $row_id])
            );
            if ($render_html !== '') {
                $this->response['html'] = $render_html;
            }
            $this->refreshFormLayoutMetadata(
                $this->resolveFormIdFromRequest(['row_id' => $row_id]),
                'builder_attach_column',
                array(
                    'row_id' => $row_id,
                    'column_id' => $column_id
                )
            );
        }

        echo wp_json_encode($this->response);
        wp_die();
    }

    public function updateRowColumnWidths()
    {
        $this->checkNonce('update_rowcolumn_widths');
        $this->checkCapabilities();

        $rowcolumn_widths = isset($_POST['column_widths']) && is_array($_POST['column_widths']) ? wp_unslash($_POST['column_widths']) : [];
        $rowcolumn_widths = array_map('absint', $rowcolumn_widths);
        
        if (!is_array($rowcolumn_widths)) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Unable to update column widths.', 'griffinforms-form-builder');
        }

        foreach ($rowcolumn_widths as $column_id => $width) {
            $this->sql->updateRowColumnWidth($column_id, $width);
        }

        $this->response['success'] = true;
        $this->response['msg'] = __('Column widths updated successfully.', 'griffinforms-form-builder');
        $this->refreshFormLayoutMetadata(
            $this->resolveFormIdFromRequest(['column_widths' => $rowcolumn_widths]),
            'builder_column_resize',
            array('column_widths' => $rowcolumn_widths)
        );

        echo wp_json_encode($this->response);
        wp_die();
    }

    public function updatePageRowColumns()
    {
        $this->checkNonce('update_pagerow_columns');
        $this->checkCapabilities();

        $row_id = isset($_POST['row_id']) ? absint(wp_unslash($_POST['row_id'])) : 0;
        $column_ids = isset($_POST['column_ids']) && is_array($_POST['column_ids']) ? array_map('absint', wp_unslash($_POST['column_ids'])) : [];

        if (!is_array($column_ids)) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Unable to update column order.', 'griffinforms-form-builder');
        }

        $update = $this->sql->updatePageRowColumns($row_id, $column_ids);

        if ($update) {
            $this->response['success'] = true;
            // translators: %d refers to the row ID for which the column order was updated
            $this->response['msg'] = sprintf(__('Column order for Row %d was updated successfully.', 'griffinforms-form-builder'), $row_id);
            $this->refreshFormLayoutMetadata(
                $this->resolveFormIdFromRequest(['row_id' => $row_id]),
                'builder_column_order',
                array('row_id' => $row_id)
            );
        } else {
            $this->response['success'] = false;
            $this->response['msg'] = __('Unable to update column order.', 'griffinforms-form-builder');
        }
        
        echo wp_json_encode($this->response);
        wp_die();
    }

    public function updateFormFormPages()
    {
        $this->checkNonce('update_form_formpages');
        $this->checkCapabilities();

        $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;
        $page_ids = isset($_POST['page_ids']) && is_array($_POST['page_ids']) ? array_map('absint', wp_unslash($_POST['page_ids'])) : [];

        if (!is_array($page_ids)) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Unable to update page order.', 'griffinforms-form-builder');
        }

        $update = $this->sql->updateFormFormPages($form_id, $page_ids);

        if ($update) {
            $this->response['success'] = true;
            // translators: %d refers to the form ID for which the page order was updated
            $this->response['msg'] = sprintf(__('Page order for Form %d was updated successfully.', 'griffinforms-form-builder'), $form_id);
            $this->refreshFormLayoutMetadata(
                $this->resolveFormIdFromRequest(['form_id' => $form_id]),
                'builder_page_order',
                array('form_id' => $form_id)
            );
        } else {
            $this->response['success'] = false;
            $this->response['msg'] = __('Unable to update page order.', 'griffinforms-form-builder');
        }
        
        echo wp_json_encode($this->response);
        wp_die();
    }

    private function resolveFormIdFromRequest(array $context = []): int
    {
        if (isset($_POST['form_id'])) {
            $form_id = absint(wp_unslash($_POST['form_id']));
            if ($form_id > 0) {
                return $form_id;
            }
        }

        if (isset($context['form_id']) && (int) $context['form_id'] > 0) {
            return (int) $context['form_id'];
        }

        if (isset($context['parent_type'], $context['parent_id'])) {
            $parent_type = (string) $context['parent_type'];
            $parent_id = (int) $context['parent_id'];
            if ($parent_type === 'form') {
                return $parent_id;
            }
            if ($parent_type === 'formpage') {
                return $this->findFormIdByPageId($parent_id);
            }
            if ($parent_type === 'pagerow') {
                return $this->findFormIdByRowId($parent_id);
            }
            if ($parent_type === 'rowcolumn') {
                return $this->findFormIdByColumnId($parent_id);
            }
        }

        if (isset($context['page_id']) && (int) $context['page_id'] > 0) {
            return $this->findFormIdByPageId((int) $context['page_id']);
        }

        if (isset($context['row_id']) && (int) $context['row_id'] > 0) {
            return $this->findFormIdByRowId((int) $context['row_id']);
        }

        if (isset($context['column_id']) && (int) $context['column_id'] > 0) {
            return $this->findFormIdByColumnId((int) $context['column_id']);
        }

        if (isset($context['column_widths']) && is_array($context['column_widths'])) {
            $column_ids = array_keys($context['column_widths']);
            $column_id = (int) ($column_ids[0] ?? 0);
            if ($column_id > 0) {
                return $this->findFormIdByColumnId($column_id);
            }
        }

        return 0;
    }

    private function findFormIdByPageId(int $page_id): int
    {
        if ($page_id <= 0) {
            return 0;
        }

        global $wpdb;
        $page_table = $this->config->getTable('formpage');
        if ($this->tableHasColumn($page_table, 'form_id')) {
            $direct = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT form_id FROM %i WHERE id = %d",
                    $page_table,
                    $page_id
                )
            );
            if (!empty($direct)) {
                return (int) $direct;
            }
        }

        $table = $this->config->getTable('form');
        $like_str = '%' . $wpdb->esc_like((string) $page_id) . '%';
        $form_id = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM %i WHERE formpages LIKE %s LIMIT 1",
                $table,
                $like_str
            )
        );

        return $form_id ? (int) $form_id : 0;
    }

    private function findFormIdByRowId(int $row_id): int
    {
        if ($row_id <= 0) {
            return 0;
        }

        global $wpdb;
        $row_table = $this->config->getTable('pagerow');
        if ($this->tableHasColumn($row_table, 'form_id')) {
            $direct = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT form_id FROM %i WHERE id = %d",
                    $row_table,
                    $row_id
                )
            );
            if (!empty($direct)) {
                return (int) $direct;
            }
        }

        $page_table = $this->config->getTable('formpage');
        $like_str = '%' . $wpdb->esc_like((string) $row_id) . '%';
        $page_id = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM %i WHERE pagerows LIKE %s LIMIT 1",
                $page_table,
                $like_str
            )
        );

        return $page_id ? $this->findFormIdByPageId((int) $page_id) : 0;
    }

    private function findFormIdByColumnId(int $column_id): int
    {
        if ($column_id <= 0) {
            return 0;
        }

        global $wpdb;
        $column_table = $this->config->getTable('rowcolumn');
        if ($this->tableHasColumn($column_table, 'form_id')) {
            $direct = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT form_id FROM %i WHERE id = %d",
                    $column_table,
                    $column_id
                )
            );
            if (!empty($direct)) {
                return (int) $direct;
            }
        }

        $row_table = $this->config->getTable('pagerow');
        $like_str = '%' . $wpdb->esc_like((string) $column_id) . '%';
        $row_id = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM %i WHERE rowcolumns LIKE %s LIMIT 1",
                $row_table,
                $like_str
            )
        );

        return $row_id ? $this->findFormIdByRowId((int) $row_id) : 0;
    }

    private function tableHasColumn(string $table, string $column): bool
    {
        static $cache = array();
        $key = $table . ':' . $column;
        if (array_key_exists($key, $cache)) {
            return $cache[$key];
        }
        global $wpdb;
        $exists = $wpdb->get_var($wpdb->prepare(
            "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = %s AND COLUMN_NAME = %s",
            $table,
            $column
        ));
        $cache[$key] = !empty($exists);
        return $cache[$key];
    }

    private function refreshFormLayoutMetadata(int $form_id, string $context = 'unknown', array $meta = array()): void
    {
        if ($form_id <= 0) {
            return;
        }

        global $wpdb;
        $form_table = $this->config->getTable('form');
        $snapshot_table = $this->config->getTable('layout_snapshot');
        $form_row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT compliance_mode, layout_version, layout_hash FROM %i WHERE id = %d",
                $form_table,
                $form_id
            ),
            ARRAY_A
        );

        if (!is_array($form_row)) {
            return;
        }

        $layout_data = $this->buildLayoutSnapshotData($form_id);
        $new_hash = $layout_data['layout_hash'];
        if ($new_hash === '') {
            return;
        }

        $current_hash = isset($form_row['layout_hash']) ? (string) $form_row['layout_hash'] : '';
        $current_version = isset($form_row['layout_version']) ? (int) $form_row['layout_version'] : 0;
        $updated_version = $current_version;

        if ($new_hash !== $current_hash) {
            $updated_version = $current_version + 1;
            $wpdb->update(
                $form_table,
                array(
                    'layout_version' => $updated_version,
                    'layout_hash' => $new_hash
                ),
                array('id' => $form_id),
                array('%d', '%s'),
                array('%d')
            );

            if (!empty($this->log)) {
                $this->log->add(
                    'info',
                    'form',
                    $form_id,
                    get_current_user_id(),
                    sprintf('Form layout updated to version %d.', $updated_version),
                    'audit'
                );
            }

            if (!empty($form_row['compliance_mode']) && $layout_data['layout_json'] !== '') {
                $wpdb->insert(
                    $snapshot_table,
                    array(
                        'form_id' => $form_id,
                        'submission_id' => null,
                        'layout_version' => $updated_version,
                        'layout_hash' => $new_hash,
                        'layout_json' => $layout_data['layout_json'],
                        'created_at' => current_time('mysql', true),
                        'created_by' => get_current_user_id()
                    ),
                    array('%d', '%d', '%d', '%s', '%s', '%s', '%d')
                );
            }

            do_action(
                'griffinforms_form_layout_changed',
                $form_id,
                $context,
                array_merge(
                    array(
                        'previous_version' => $current_version,
                        'new_version' => $updated_version,
                        'previous_hash' => $current_hash,
                        'new_hash' => $new_hash,
                        'compliance_mode' => !empty($form_row['compliance_mode']) ? 1 : 0
                    ),
                    $meta
                )
            );
        }
    }

    private function buildLayoutSnapshotData(int $form_id): array
    {
        $result = array(
            'layout_payload' => array(),
            'layout_json' => '',
            'layout_hash' => ''
        );

        if ($form_id <= 0) {
            return $result;
        }

        try {
            $exporter = new \GriffinForms\Includes\ImportExport\FormExport($form_id);
            $payload = $exporter->getFormData();
            if (!is_array($payload)) {
                return $result;
            }

            $payload = $this->normalizeLayoutPayload($payload);
            $result['layout_payload'] = $payload;
            $result['layout_json'] = \GriffinForms\Includes\Helpers\Compliance::canonicalJson($payload);
            $result['layout_hash'] = \GriffinForms\Includes\Helpers\Compliance::hashLayout($payload);
        } catch (\Throwable $t) {
            if (!empty($this->log)) {
                $this->log->add(
                    'warning',
                    'form',
                    $form_id,
                    get_current_user_id(),
                    'Unable to compute form layout snapshot: ' . $t->getMessage(),
                    'audit'
                );
            }
        }

        return $result;
    }

    private function normalizeLayoutPayload(array $payload): array
    {
        $strip_keys = array(
            'author',
            'created',
            'last_editor',
            'last_edited',
            'created_at',
            'updated_at',
            'lifetime_submission_count',
            'layout_version',
            'layout_hash',
            'compliance_mode',
            'compliance_profile',
            'compliance_flags'
        );

        return \GriffinForms\Includes\Helpers\Compliance::stripKeysRecursive($payload, $strip_keys);
    }

    public function getJs()
    {
        $this->getFormLayoutJs();
        $this->aiDraftBannerJs();
        $this->formElementClickJs();
        $this->formElementHoverJs();
        $this->updateItemNameJs();
        $this->updateItemHeadingJs();
        $this->updateItemDescriptionJs();
        $this->updatePageStepDataJs();
        $this->builderWarningCountJs();
        $this->appendFormIdToEditLinksJs();
        $this->showAddPageJs();
        $this->insertNewPageBtnJs();
        $this->appendNewRowBtnJs();
        $this->makeColumnsDroppableJs();
        $this->highlightEmptyColumnsJs();
        $this->highlightEmptyRowsJs();
        $this->insertNewFieldJs();
        $this->makePageRowsSortableJs();
        $this->updateFormPageRowsJs();
        $this->makeFieldsSortableJs();
        $this->updateRowColumnFieldsJs();
        $this->makeRowColumnsResizableJs();
        $this->toggleCreateRowColumnBtnJs();
        $this->createRowColumnBtnJs();
        $this->moveRowColumnBtnJs();
        $this->updateRowColumnWidthsJs();
        $this->makeRowColumnsSortableJs();
        $this->updatePageRowColumnsJs();
        $this->moveRowBtnJs();
        $this->movePageBtnJs();
        $this->makeFormPagesSortableJs();
        $this->updatePageUiJs();
        $this->choiceFieldInitJs();
        $this->dropdownFieldInitJs();
        $this->paymentFieldInitJs();
        $this->navButtonInlineEditJs();
    }

    /**
     * All JS for inline choice-field option editing (radio + checkbox).
     */
    private function choiceFieldInitJs()
    {
        $nonce = wp_create_nonce('update_field_options');

        echo '
        // ─── Choice field option editing ─────────────────────────────────────

        var gfChoiceNonce = "' . esc_js($nonce) . '";

        var gfChoiceI18n = {
            newOptionLabel:  ' . json_encode(__('Option', 'griffinforms-form-builder')) . ',
            valueEmptyError: ' . json_encode(__('Value cannot be empty after sanitization.', 'griffinforms-form-builder')) . ',
            ariaAddOption:   ' . json_encode(__('Add option', 'griffinforms-form-builder')) . ',
        };

        // Collect field_options from DOM as { "0": {"Label": "value"}, ... }
        function gfCollectFieldOptions(fieldWrap) {
            var options = {};
            var idx = 0;
            fieldWrap.find(".gf-choice-field-column:not(.gf-choice-field-temp-column)").each(function() {
                var group = [];
                $(this).find(".gf-choice-option-row").each(function() {
                    var label = $(this).attr("data-gf-option-label") || "";
                    var value = $(this).attr("data-gf-option-value") || "";
                    if (label !== "") {
                        group.push([label, value]);
                    }
                });
                if (group.length > 0) {
                    options[idx] = group;
                    idx++;
                }
            });
            return options;
        }

        // Collect field_options from dropdown DOM — string group keys (for select fields)
        function gfCollectDropdownOptions(fieldWrap) {
            var options = {};
            fieldWrap.find(".gf-dropdown-option-group").each(function() {
                var groupKey = $(this).attr("data-gf-group-key");
                if (typeof groupKey === "undefined") { return; }
                var group = [];
                $(this).find(".gf-choice-option-row").each(function() {
                    var label = $(this).attr("data-gf-option-label") || "";
                    var value = $(this).attr("data-gf-option-value") || "";
                    if (label !== "") {
                        group.push([label, value]);
                    }
                });
                // Always keep the "" (ungrouped) group; skip empty named groups
                if (groupKey === "" || group.length > 0) {
                    options[groupKey] = group;
                }
            });
            return options;
        }

        // Save options to server; optionally reconcile DOM on success
        var gfSaveXhrByField = {};
        function gfSaveFieldOptions(fieldWrap, onSuccess) {
            var fieldId   = fieldWrap.data("gf-field-id");
            if (!fieldId) { return; } // no valid field wrap

            if (gfSaveXhrByField[fieldId]) {
                gfSaveXhrByField[fieldId].abort();
                delete gfSaveXhrByField[fieldId];
            }

            var fieldType = fieldWrap.closest("[data-gf-fieldtype]").attr("data-gf-fieldtype") || "";
            var options   = fieldWrap.hasClass("gf-dropdown-field-wrap")
                ? gfCollectDropdownOptions(fieldWrap)
                : gfCollectFieldOptions(fieldWrap);

            statusbarMsg("waiting");

            gfSaveXhrByField[fieldId] = $.ajax({
                url:  ajaxurl,
                type: "POST",
                data: {
                    action:        "updateFieldOptions",
                    field_id:      fieldId,
                    field_type:    fieldType,
                    field_options: JSON.stringify(options),
                    nonce:         gfChoiceNonce
                },
                success: function(response) {
                    delete gfSaveXhrByField[fieldId];
                    response = JSON.parse(response);
                    if (response.success) {
                        statusbarMsg("success", response.message + " @ " + response.time);
                        if (fieldWrap.hasClass("gf-dropdown-field-wrap")) {
                            gfSyncDropdownSelectPreview(fieldWrap, response.field_options);
                        }
                        if (typeof onSuccess === "function") onSuccess(response.field_options);
                    } else {
                        statusbarMsg("warning", response.message);
                    }
                },
                error: function(xhr, status) {
                    delete gfSaveXhrByField[fieldId];
                    if (status !== "abort") {
                        statusbarMsg("warning", "Save failed. Please try again.");
                    }
                }
            });
        }

        // Derive slug from label (mirrors server: lowercase, strip spaces)
        function gfDeriveOptionValue(label) {
            return label.toLowerCase().replace(/\s+/g, "");
        }

        // Rebuild the native <select> preview from server-confirmed field_options
        function gfSyncDropdownSelectPreview(fieldWrap, savedOptions) {
            var $select = fieldWrap.find(".gf-dropdown-select-preview");
            if (!$select.length || !savedOptions) { return; }
            var $placeholder = $select.find("option[value=\'\']").first().detach();
            $select.empty();
            if ($placeholder.length) { $select.prepend($placeholder); }
            $.each(savedOptions, function(groupKey, options) {
                if (groupKey === "") {
                    $.each(options, function(label, value) {
                        $select.append($("<option>").val(value).text(label));
                    });
                } else {
                    var $optgroup = $("<optgroup>").attr("label", groupKey);
                    $.each(options, function(label, value) {
                        $optgroup.append($("<option>").val(value).text(label));
                    });
                    $select.append($optgroup);
                }
            });
        }

        // Recount total options and update delete/move button disabled states
        function gfRefreshDeleteGuards(fieldWrap) {
            var rows  = fieldWrap.find(".gf-choice-field-column:not(.gf-choice-field-temp-column) .gf-choice-option-row");
            var total = rows.length;
            fieldWrap.find(".gf-choice-option-delete").prop("disabled", total <= 1);
            rows.each(function(i) {
                $(this).find(".gf-choice-option-move-up").prop("disabled", i === 0);
                $(this).find(".gf-choice-option-move-down").prop("disabled", i === total - 1);
            });
        }

        // Update group indexes on column data attributes after DOM changes
        function gfRefreshGroupIndexes(fieldWrap) {
            fieldWrap.find(".gf-choice-field-column:not(.gf-choice-field-temp-column)").each(function(i) {
                $(this).attr("data-gf-option-group", i);
                $(this).find(".gf-choice-field-add-option-btn").attr("data-gf-option-group", i);
            });
        }

        // ─── Clear option selection when parent field is deselected ──────────
        document.addEventListener("item-selected", function(e) {
            if (!e.detail || e.detail.itemType === "field") {
                return;
            }
            $(".gf-choice-option-row--focused").each(function() {
                $(this).removeClass("gf-choice-option-row--focused");
                $(this).find(".gf-choice-option-value-popover").prop("hidden", true);
            });
        });

        // ─── Toolbar show/hide (click-to-select, one at a time) ──────────────

        function gfSelectOptionRow(row) {
            // Deselect ALL focused option rows across the entire builder
            $(".gf-choice-option-row--focused").not(row).each(function() {
                $(this).removeClass("gf-choice-option-row--focused");
                $(this).find(".gf-choice-option-value-popover").prop("hidden", true);
            });
            row.addClass("gf-choice-option-row--focused");
        }

        function gfDeselectAllOptionRows(fieldWrap) {
            fieldWrap.find(".gf-choice-option-row--focused").each(function() {
                $(this).removeClass("gf-choice-option-row--focused");
                $(this).find(".gf-choice-option-value-popover").prop("hidden", true);
            });
        }

        // Select row on click or keyboard focus
        $(document).on("mousedown focusin", ".gf-choice-option-row", function() {
            gfSelectOptionRow($(this));
        });

        // Deselect when clicking outside any option row within the field wrap
        // Do not stopPropagation when drag handle is clicked — sortable needs the event
        $(document).on("mousedown", ".gf-choice-field-wrap", function(e) {
            var isDragHandle = $(e.target).closest(".gf-choice-option-drag-handle").length > 0;
            if (!isDragHandle) {
                e.stopPropagation();
            }
            if (!$(e.target).closest(".gf-choice-option-row").length) {
                gfDeselectAllOptionRows($(this));
            }
        });

        // Deselect all focused rows when clicking outside any choice/dropdown field wrap
        $(document).on("mousedown.gf-choice", function(e) {
            if (!$(e.target).closest(".gf-choice-field-wrap, .gf-dropdown-field-wrap").length) {
                $(".gf-choice-option-row--focused").each(function() {
                    $(this).removeClass("gf-choice-option-row--focused");
                    $(this).find(".gf-choice-option-value-popover").prop("hidden", true);
                });
            }
        });

        // When the main field toolbar (action cluster) is used, collapse any open
        // option sub-toolbar and close any open dropdown panel so the toolbar action
        // fires cleanly on the next click (or this one if z-index fix is sufficient)
        $(document).on("mousedown", ".gf-fe-action-cluster, .gf-fe-action-menu", function() {
            $(".gf-choice-option-row--focused").removeClass("gf-choice-option-row--focused");
            $(".gf-dropdown-field-wrap").each(function() {
                gfCloseDropdownPanel($(this));
            });
        });

        // ─── Label editing ────────────────────────────────────────────────────

        // Derive value on input
        $(document).on("input", ".gf-choice-option-label", function() {
            var span  = $(this);
            var label = span.text().trim();
            var value = gfDeriveOptionValue(label);
            var row   = span.closest(".gf-choice-option-row");
            row.attr("data-gf-option-value", value);
            row.find(".gf-choice-option-value-input").val(value);
            row.find("input.gf-checkbox-radio-input").val(value);
        });

        // Save on blur
        $(document).on("blur", ".gf-choice-option-label", function() {
            var span  = $(this);
            var label = span.text().trim();
            var prev  = span.attr("data-gf-prev-value") || "";
            var row   = span.closest(".gf-choice-option-row");

            if (label === "") {
                // Revert to previous value if blank
                span.text(prev);
                row.attr("data-gf-option-label", prev);
                row.attr("data-gf-option-value", gfDeriveOptionValue(prev));
                return;
            }

            row.attr("data-gf-option-label", label);
            span.attr("data-gf-prev-value", label);

            var fieldWrap = span.closest(".gf-choice-field-wrap");
            gfSaveFieldOptions(fieldWrap);
        });

        // Prevent Enter from inserting newlines; trigger blur instead
        $(document).on("keydown", ".gf-choice-option-label", function(e) {
            if (e.key === "Enter") {
                e.preventDefault();
                $(this).trigger("blur");
            }
        });

        // ─── Add option ───────────────────────────────────────────────────────
        $(document).on("click", ".gf-choice-field-add-option-btn", function(e) {
            e.stopPropagation();
            var btn       = $(this);
            var column    = btn.closest(".gf-choice-field-column");
            var fieldWrap = btn.closest(".gf-choice-field-wrap");
            var prefix    = gfChoiceI18n.newOptionLabel + " ";
            var maxN      = 0;
            fieldWrap.find(".gf-choice-field-column:not(.gf-choice-field-temp-column) .gf-choice-option-row").each(function() {
                var lbl = $(this).attr("data-gf-option-label") || "";
                if (lbl.indexOf(prefix) === 0) {
                    var n = parseInt(lbl.slice(prefix.length), 10);
                    if (!isNaN(n) && n > maxN) { maxN = n; }
                }
            });
            var newLabel  = prefix + (maxN + 1);
            var newValue  = gfDeriveOptionValue(newLabel);

            // Clone first existing option row to get the same input type/name
            var template  = fieldWrap.find(".gf-choice-option-row").first();
            var newRow    = template.clone(false);

            newRow.attr("data-gf-option-label", newLabel);
            newRow.attr("data-gf-option-value", newValue);
            newRow.removeClass("gf-choice-option-row--focused");
            newRow.find(".gf-choice-option-label").text(newLabel).attr("data-gf-prev-value", newLabel);
            newRow.find("input.gf-checkbox-radio-input").val(newValue);
            newRow.find(".gf-choice-option-value-input").val(newValue);
            newRow.find(".gf-choice-option-value-popover").prop("hidden", true);
            newRow.find(".gf-choice-option-toolbar").removeClass("gf-sub-toolbar--visible");

            // Insert before the add-option-wrap
            btn.closest(".gf-choice-field-add-option-wrap").before(newRow);

            gfRefreshDeleteGuards(fieldWrap);

            gfSaveFieldOptions(fieldWrap, function() {
                // Focus new label after save
                newRow.find(".gf-choice-option-label").focus();
            });
        });

        // ─── Delete option ────────────────────────────────────────────────────
        $(document).on("click", ".gf-choice-option-delete", function(e) {
            e.stopPropagation();
            var btn       = $(this);
            var row       = btn.closest(".gf-choice-option-row");
            var fieldWrap = btn.closest(".gf-choice-field-wrap");
            var column    = row.closest(".gf-choice-field-column");

            row.remove();

            // Remove empty columns (but keep at least one)
            column.find(".gf-choice-option-row").length === 0 && column.remove();

            gfRefreshGroupIndexes(fieldWrap);
            gfRefreshDeleteGuards(fieldWrap);
            gfSaveFieldOptions(fieldWrap);
        });

        // ─── Move option up ───────────────────────────────────────────────────
        $(document).on("click", ".gf-choice-option-move-up", function(e) {
            e.stopPropagation();
            var row       = $(this).closest(".gf-choice-option-row");
            var column    = row.closest(".gf-choice-field-column");
            var prev      = row.prev(".gf-choice-option-row");
            var fieldWrap = row.closest(".gf-choice-field-wrap");

            if (prev.length) {
                row.insertBefore(prev);
            } else {
                // Move to bottom of previous column
                var prevCol = column.prev(".gf-choice-field-column");
                if (prevCol.length) {
                    row.insertBefore(prevCol.find(".gf-choice-field-add-option-wrap"));
                }
            }

            gfRefreshGroupIndexes(fieldWrap);
            gfRefreshDeleteGuards(fieldWrap);
            gfSaveFieldOptions(fieldWrap);
        });

        // ─── Move option down ─────────────────────────────────────────────────
        $(document).on("click", ".gf-choice-option-move-down", function(e) {
            e.stopPropagation();
            var row       = $(this).closest(".gf-choice-option-row");
            var column    = row.closest(".gf-choice-field-column");
            var next      = row.next(".gf-choice-option-row");
            var fieldWrap = row.closest(".gf-choice-field-wrap");

            if (next.length) {
                row.insertAfter(next);
            } else {
                // Move to top of next column
                var nextCol = column.next(".gf-choice-field-column");
                if (nextCol.length) {
                    nextCol.prepend(row);
                }
            }

            gfRefreshGroupIndexes(fieldWrap);
            gfRefreshDeleteGuards(fieldWrap);
            gfSaveFieldOptions(fieldWrap);
        });

        // ─── Value popover ────────────────────────────────────────────────────
        $(document).on("click", ".gf-choice-option-value-btn", function(e) {
            e.stopPropagation();
            var btn     = $(this);
            var row     = btn.closest(".gf-choice-option-row");
            var popover = row.find(".gf-choice-option-value-popover");
            var isHidden = popover.prop("hidden");

            // Close all other popovers
            $(".gf-choice-option-value-popover").prop("hidden", true);

            if (isHidden) {
                popover.prop("hidden", false);
                popover.find(".gf-choice-option-value-input").focus().select();
            }
        });

        // Save value on popover input blur
        $(document).on("blur", ".gf-choice-option-value-input", function() {
            var input     = $(this);
            var row       = input.closest(".gf-choice-option-row");
            var fieldWrap = input.closest(".gf-choice-field-wrap");
            var rawValue  = input.val().trim().toLowerCase().replace(/[^a-z0-9_\-]/g, "");
            var errDiv    = input.siblings(".gf-choice-option-value-error");

            if (rawValue === "") {
                errDiv.text(gfChoiceI18n.valueEmptyError).prop("hidden", false);
                input.val(row.attr("data-gf-option-value"));
                return;
            }

            errDiv.prop("hidden", true);
            row.attr("data-gf-option-value", rawValue);
            row.find("input.gf-checkbox-radio-input").val(rawValue);
            input.val(rawValue);

            setTimeout(function() {
                row.find(".gf-choice-option-value-popover").prop("hidden", true);
            }, 100);

            gfSaveFieldOptions(fieldWrap);
        });

        // Close popover on Escape
        $(document).on("keydown", ".gf-choice-option-value-input", function(e) {
            if (e.key === "Escape") {
                $(this).closest(".gf-choice-option-value-popover").prop("hidden", true);
            }
            if (e.key === "Enter") {
                e.preventDefault();
                $(this).trigger("blur");
            }
        });

        // ─── Drag/sort (jQuery UI sortable) ───────────────────────────────────
        function gfInitChoiceSortable(fieldWrap) {
            fieldWrap.find(".gf-choice-field-column").sortable({
                items: ".gf-choice-option-row",
                handle: ".gf-choice-option-drag-handle",
                cancel: "input, textarea, select, option, button:not(.gf-choice-option-drag-handle)",
                connectWith: fieldWrap.find(".gf-choice-field-column"),
                axis: false,
                tolerance: "pointer",
                helper: "clone",
                start: function(event, ui) {
                    // Deselect any focused option row before dragging
                    gfDeselectAllOptionRows(fieldWrap);
                    // Hide add-option buttons during drag
                    fieldWrap.addClass("gf-choice-field-wrap--dragging");
                    // Only offer a new column drop zone if the dragged item is not the
                    // sole option in the last column (moving it would leave nothing behind)
                    var realCols = fieldWrap.find(".gf-choice-field-column:not(.gf-choice-field-temp-column)");
                    var sourceCol = ui.item.closest(".gf-choice-field-column");
                    var isLastCol = sourceCol.is(realCols.last());
                    var isOnlyChild = sourceCol.find(".gf-choice-option-row").length === 1;
                    var showTempCol = !(isLastCol && isOnlyChild);
                    // Add temp column as drop zone and initialize it as sortable
                    var tempCol = $();
                    if (showTempCol) {
                        tempCol = $("<div>").addClass("gf-choice-field-column gf-choice-field-temp-column flex-column me-3");
                        fieldWrap.append(tempCol);
                        var allCols = fieldWrap.find(".gf-choice-field-column");
                        tempCol.sortable({
                            items: ".gf-choice-option-row",
                            handle: ".gf-choice-option-drag-handle",
                            cancel: "input, textarea, select, option, button:not(.gf-choice-option-drag-handle)",
                            connectWith: allCols,
                            tolerance: "pointer",
                            helper: "clone",
                            over: function() {
                                tempCol.addClass("gf-choice-field-temp-column--active");
                            },
                            out: function() {
                                tempCol.removeClass("gf-choice-field-temp-column--active");
                            }
                        });
                        // Refresh connectWith on existing columns to include temp column
                        allCols.not(tempCol).sortable("option", "connectWith", allCols);
                        allCols.not(tempCol).sortable("refresh");
                    }
                },
                sort: function(event, ui) {
                    var addWrap = $(this).find("> .gf-choice-field-add-option-wrap");
                    if (addWrap.length && ui.placeholder.index() > addWrap.index()) {
                        addWrap.before(ui.placeholder);
                    }
                },
                stop: function(event, ui) {
                    // Restore add-option buttons
                    fieldWrap.removeClass("gf-choice-field-wrap--dragging");
                    var tempCol = fieldWrap.find(".gf-choice-field-temp-column");
                    // If item was dropped into the temp column, promote it to a real column
                    if (ui.item.closest(".gf-choice-field-temp-column").length) {
                        tempCol.removeClass("gf-choice-field-temp-column gf-choice-field-temp-column--active");
                        // Add the add-option button to the new real column
                        var groupIndex = fieldWrap.find(".gf-choice-field-column").length - 1;
                        tempCol.append(
                            $("<div>").addClass("gf-choice-field-add-option-wrap").append(
                                $("<div>").addClass("gf-choice-field-add-option-bar")
                            ).append(
                                $("<button>").attr({
                                    type: "button",
                                    "class": "gf-choice-field-add-option-btn",
                                    "aria-label": gfChoiceI18n.ariaAddOption,
                                    "data-gf-option-group": groupIndex
                                }).append($("<span>").addClass("gf-choice-add-option-icon").text("+"))
                            )
                        );
                    } else {
                        tempCol.sortable("destroy");
                        tempCol.remove();
                    }
                    // Ensure dropped item sits above the add-option wrap only if it landed past it
                    var addWrap = ui.item.closest(".gf-choice-field-column").find(".gf-choice-field-add-option-wrap");
                    if (addWrap.length && ui.item.index() > addWrap.index()) {
                        addWrap.before(ui.item);
                    }
                    // Remove any real columns that are now empty (no option rows)
                    fieldWrap.find(".gf-choice-field-column:not(.gf-choice-field-temp-column)").each(function() {
                        if ($(this).find(".gf-choice-option-row").length === 0) {
                            if ($(this).is(".ui-sortable")) {
                                $(this).sortable("destroy");
                            }
                            $(this).remove();
                        }
                    });
                    gfRefreshGroupIndexes(fieldWrap);
                    gfRefreshDeleteGuards(fieldWrap);
                    gfSaveFieldOptions(fieldWrap);
                    // Re-init sortables so next drag gets a fresh temp column
                    gfInitChoiceSortable(fieldWrap);
                    // Landing flash
                    ui.item.addClass("gf-choice-option-row--landing");
                    ui.item.one("animationend", function() {
                        $(this).removeClass("gf-choice-option-row--landing");
                    });
                }
            });
        }

        // Initialize sortable on all choice field wraps; safe to call repeatedly
        function gfInitAllChoiceSortables() {
            $(".gf-choice-field-wrap").each(function() {
                var cols = $(this).find(".gf-choice-field-column");
                cols.each(function() {
                    if ($(this).is(".ui-sortable")) {
                        $(this).sortable("destroy");
                    }
                });
                gfInitChoiceSortable($(this));
            });
        }

        document.addEventListener("form-html-received", function() {
            requestAnimationFrame(function() {
                gfInitAllChoiceSortables();
            });
        });

        ';
    }

    /**
     * All JS for inline dropdown (select) option editing.
     */
    private function dropdownFieldInitJs()
    {
        echo '
        // ─── Dropdown field option editing ────────────────────────────────────

        var gfDropdownI18n = {
            newOptionLabel:  ' . json_encode(__('Option', 'griffinforms-form-builder')) . ',
            newGroupLabel:   ' . json_encode(__('Group', 'griffinforms-form-builder')) . ',
            valueEmptyError: ' . json_encode(__('Value cannot be empty after sanitization.', 'griffinforms-form-builder')) . ',
            ariaDeleteGroup: ' . json_encode(__('Delete group', 'griffinforms-form-builder')) . ',
            ariaAddOption:   ' . json_encode(__('Add option', 'griffinforms-form-builder')) . ',
        };


        // Remove empty named groups and update grouped state
        function gfDropdownCleanEmptyGroups(fieldWrap) {
            var panel    = fieldWrap.find(".gf-dropdown-options-panel");
            panel.find(".gf-dropdown-option-group").each(function() {
                var key = $(this).attr("data-gf-group-key");
                if (key !== "" && $(this).find(".gf-choice-option-row").length === 0) {
                    $(this).remove();
                }
            });
            var hasNamed = panel.find(".gf-dropdown-option-group").filter(function() {
                return $(this).attr("data-gf-group-key") !== "";
            }).length > 0;
            panel.attr("data-gf-grouped", hasNamed ? "true" : "false");
        }

        // Refresh delete/move-up/move-down guards for dropdown fields
        function gfRefreshDropdownDeleteGuards(fieldWrap) {
            var rows  = fieldWrap.find(".gf-dropdown-option-group .gf-choice-option-row");
            var total = rows.length;
            fieldWrap.find(".gf-dropdown-options-panel .gf-choice-option-delete").prop("disabled", total <= 1);
            rows.each(function(i) {
                $(this).find(".gf-choice-option-move-up").prop("disabled", i === 0);
                $(this).find(".gf-choice-option-move-down").prop("disabled", i === total - 1);
            });
        }

        // Update data-gf-group-key on all groups (no-op here since keys are strings, but refreshes add-option btn attrs)
        function gfRefreshDropdownGroupKeys(fieldWrap) {
            fieldWrap.find(".gf-dropdown-option-group").each(function() {
                var key = $(this).attr("data-gf-group-key");
                $(this).find(".gf-choice-field-add-option-btn").attr("data-gf-group-key", key);
            });
        }

        // Open the panel for a given fieldWrap
        function gfOpenDropdownPanel(fieldWrap) {
            // Close any other open dropdown panel first
            $(".gf-dropdown-field-wrap").not(fieldWrap).each(function() {
                gfCloseDropdownPanel($(this));
            });

            var panel = fieldWrap.find(".gf-dropdown-options-panel");

            // Determine grouped state
            var hasNamed = panel.find(".gf-dropdown-option-group").filter(function() {
                return $(this).attr("data-gf-group-key") !== "";
            }).length > 0;
            panel.attr("data-gf-grouped", hasNamed ? "true" : "false");

            // Expose select border-width as CSS variable so the panel can overlap it.
            // Also force the panel background opaque: some themes use rgba bg colors which
            // would make the panel see-through, exposing builder elements behind it.
            var selectEl = fieldWrap.find(".gf-dropdown-select-preview")[0];
            if (selectEl) {
                var cs  = window.getComputedStyle(selectEl);
                var bw  = parseFloat(cs.borderBottomWidth) || 1;
                fieldWrap[0].style.setProperty("--gf-select-border-width", bw + "px");

                var bg     = cs.backgroundColor;
                var rgba   = bg.match(/rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)/i);
                var opaque = (rgba && parseFloat(rgba[4]) < 1)
                    ? "rgb(" + rgba[1] + "," + rgba[2] + "," + rgba[3] + ")"
                    : bg;
                panel[0].style.setProperty("background-color", opaque, "important");
            }

            panel.prop("hidden", false).addClass("gf-dropdown-options-panel--open");

            // Viewport overflow: if panel bottom exceeds viewport, open upward
            var rect = panel[0].getBoundingClientRect();
            if (rect.bottom > window.innerHeight) {
                panel.css({ top: "auto", bottom: "100%" });
                panel.addClass("gf-dropdown-options-panel--upward");
            } else {
                panel.css({ top: "", bottom: "auto" });
                panel.removeClass("gf-dropdown-options-panel--upward");
            }

            gfRefreshDropdownDeleteGuards(fieldWrap);
            gfInitDropdownSortable(fieldWrap);
        }

        // Close the panel for a given fieldWrap
        function gfCloseDropdownPanel(fieldWrap) {
            var panel = fieldWrap.find(".gf-dropdown-options-panel");
            if (!panel.hasClass("gf-dropdown-options-panel--open")) { return; }

            // Revert any in-progress label edit without saving
            panel.find(".gf-choice-option-label:focus, .gf-dropdown-group-name:focus").each(function() {
                $(this).text($(this).attr("data-gf-prev-value") || "");
                this.blur();
            });

            panel.prop("hidden", true).removeClass("gf-dropdown-options-panel--open gf-dropdown-options-panel--upward");
            panel.css({ top: "", bottom: "" });
            gfDeselectAllOptionRows(fieldWrap);
        }

        // Toggle panel on overlay click
        $(document).on("click", ".gf-dropdown-trigger-overlay", function(e) {
            e.stopPropagation();
            var fieldWrap = $(this).closest(".gf-dropdown-field-wrap");
            var panel     = fieldWrap.find(".gf-dropdown-options-panel");
            if (panel.hasClass("gf-dropdown-options-panel--open")) {
                gfCloseDropdownPanel(fieldWrap);
            } else {
                gfOpenDropdownPanel(fieldWrap);
            }
        });

        // Close on mousedown outside the dropdown field wrap
        $(document).on("mousedown.gf-dropdown", function(e) {
            if ($(e.target).closest(".gf-dropdown-field-wrap").length === 0) {
                $(".gf-dropdown-field-wrap").each(function() {
                    gfCloseDropdownPanel($(this));
                });
            }
        });

        // Close on Escape key
        $(document).on("keydown.gf-dropdown", function(e) {
            if (e.key === "Escape") {
                $(".gf-dropdown-options-panel--open").each(function() {
                    gfCloseDropdownPanel($(this).closest(".gf-dropdown-field-wrap"));
                });
            }
        });

        // Close when builder deselects the field
        document.addEventListener("item-selected", function(e) {
            if (!e.detail || e.detail.itemType === "field") { return; }
            $(".gf-dropdown-field-wrap").each(function() {
                gfCloseDropdownPanel($(this));
            });
        });

        // ─── Add option (dropdown) ────────────────────────────────────────────
        $(document).on("click", ".gf-dropdown-options-panel .gf-choice-field-add-option-btn", function(e) {
            e.stopPropagation();
            var btn       = $(this);
            var group     = btn.closest(".gf-dropdown-option-group");
            var fieldWrap = btn.closest(".gf-dropdown-field-wrap");
            var prefix    = gfDropdownI18n.newOptionLabel + " ";
            var maxN      = 0;
            fieldWrap.find(".gf-dropdown-option-group .gf-choice-option-row").each(function() {
                var lbl = $(this).attr("data-gf-option-label") || "";
                if (lbl.indexOf(prefix) === 0) {
                    var n = parseInt(lbl.slice(prefix.length), 10);
                    if (!isNaN(n) && n > maxN) { maxN = n; }
                }
            });
            var newLabel  = prefix + (maxN + 1);
            var newValue  = gfDeriveOptionValue(newLabel);

            // Clone first row in this group, or any existing row
            var template  = group.find(".gf-choice-option-row").first();
            if (!template.length) {
                template = fieldWrap.find(".gf-choice-option-row").first();
            }

            var newRow = template.clone(false);
            newRow.attr("data-gf-option-label", newLabel);
            newRow.attr("data-gf-option-value", newValue);
            newRow.removeClass("gf-choice-option-row--focused");
            newRow.find(".gf-choice-option-label").text(newLabel).attr("data-gf-prev-value", newLabel);
            newRow.find(".gf-choice-option-value-input").val(newValue);
            newRow.find(".gf-choice-option-value-popover").prop("hidden", true);
            newRow.find(".gf-choice-option-toolbar").removeClass("gf-sub-toolbar--visible");
            newRow.find(".gf-choice-option-delete").prop("disabled", false);

            btn.closest(".gf-choice-field-add-option-wrap").before(newRow);

            gfRefreshDropdownDeleteGuards(fieldWrap);
            gfSaveFieldOptions(fieldWrap, function() {
                newRow.find(".gf-choice-option-label").focus();
            });
        });

        // ─── Delete option (dropdown) ─────────────────────────────────────────
        $(document).on("click", ".gf-dropdown-options-panel .gf-choice-option-delete", function(e) {
            e.stopPropagation();
            var btn       = $(this);
            var row       = btn.closest(".gf-choice-option-row");
            var group     = row.closest(".gf-dropdown-option-group");
            var fieldWrap = row.closest(".gf-dropdown-field-wrap");
            var groupKey  = group.attr("data-gf-group-key");

            row.remove();

            // If a named group is now empty, remove it
            if (groupKey !== "" && group.find(".gf-choice-option-row").length === 0) {
                group.remove();
                // If no named groups remain, switch to simple mode
                var hasNamed = fieldWrap.find(".gf-dropdown-option-group").filter(function() {
                    return $(this).attr("data-gf-group-key") !== "";
                }).length > 0;
                if (!hasNamed) {
                    fieldWrap.find(".gf-dropdown-options-panel").attr("data-gf-grouped", "false");
                }
            }

            gfRefreshDropdownDeleteGuards(fieldWrap);
            gfSaveFieldOptions(fieldWrap);
        });

        // ─── Move option up (dropdown) ────────────────────────────────────────
        $(document).on("click", ".gf-dropdown-options-panel .gf-choice-option-move-up", function(e) {
            e.stopPropagation();
            var row       = $(this).closest(".gf-choice-option-row");
            var group     = row.closest(".gf-dropdown-option-group");
            var fieldWrap = row.closest(".gf-dropdown-field-wrap");
            var prev      = row.prev(".gf-choice-option-row");

            if (prev.length) {
                row.insertBefore(prev);
            } else {
                // Move to bottom of previous group
                var prevGroup = group.prev(".gf-dropdown-option-group");
                if (prevGroup.length) {
                    row.insertBefore(prevGroup.find(".gf-choice-field-add-option-wrap"));
                }
            }

            gfDropdownCleanEmptyGroups(fieldWrap);
            gfRefreshDropdownDeleteGuards(fieldWrap);
            gfSaveFieldOptions(fieldWrap);
        });

        // ─── Move option down (dropdown) ──────────────────────────────────────
        $(document).on("click", ".gf-dropdown-options-panel .gf-choice-option-move-down", function(e) {
            e.stopPropagation();
            var row       = $(this).closest(".gf-choice-option-row");
            var group     = row.closest(".gf-dropdown-option-group");
            var fieldWrap = row.closest(".gf-dropdown-field-wrap");
            var next      = row.next(".gf-choice-option-row");

            if (next.length) {
                row.insertAfter(next);
            } else {
                // Move to top of next group
                var nextGroup = group.next(".gf-dropdown-option-group");
                if (nextGroup.length) {
                    nextGroup.prepend(row);
                }
            }

            gfDropdownCleanEmptyGroups(fieldWrap);
            gfRefreshDropdownDeleteGuards(fieldWrap);
            gfSaveFieldOptions(fieldWrap);
        });

        // ─── Label blur/save (dropdown) — reuses shared .gf-choice-option-label handlers;
        //     only need to override fieldWrap lookup since panel rows are in .gf-dropdown-field-wrap.
        //     The existing $(document).on("blur", ".gf-choice-option-label") handler calls
        //     span.closest(".gf-choice-field-wrap") which returns empty for dropdown rows.
        //     We add a dropdown-specific blur handler here that fires for the same element.
        $(document).on("blur.gf-dropdown", ".gf-dropdown-options-panel .gf-choice-option-label", function() {
            var span      = $(this);
            var label     = span.text().trim();
            var prev      = span.attr("data-gf-prev-value") || "";
            var row       = span.closest(".gf-choice-option-row");
            var fieldWrap = span.closest(".gf-dropdown-field-wrap");

            if (label === "") {
                span.text(prev);
                row.attr("data-gf-option-label", prev);
                row.attr("data-gf-option-value", gfDeriveOptionValue(prev));
                return;
            }

            row.attr("data-gf-option-label", label);
            span.attr("data-gf-prev-value", label);
            gfSaveFieldOptions(fieldWrap);
        });

        // Value popover save also needs dropdown fieldWrap — the existing blur handler uses
        // input.closest(".gf-choice-field-wrap") which does not match dropdown rows.
        $(document).on("blur.gf-dropdown", ".gf-dropdown-options-panel .gf-choice-option-value-input", function() {
            var input     = $(this);
            var row       = input.closest(".gf-choice-option-row");
            var fieldWrap = input.closest(".gf-dropdown-field-wrap");
            var rawValue  = input.val().trim().toLowerCase().replace(/[^a-z0-9_\-]/g, "");
            var errDiv    = input.siblings(".gf-choice-option-value-error");

            if (rawValue === "") {
                errDiv.text(gfDropdownI18n.valueEmptyError).prop("hidden", false);
                input.val(row.attr("data-gf-option-value"));
                return;
            }

            errDiv.prop("hidden", true);
            row.attr("data-gf-option-value", rawValue);
            input.val(rawValue);

            setTimeout(function() {
                row.find(".gf-choice-option-value-popover").prop("hidden", true);
            }, 100);

            gfSaveFieldOptions(fieldWrap);
        });

        // ─── Optgroup: group name editing ─────────────────────────────────────
        $(document).on("blur", ".gf-dropdown-group-name", function() {
            var span      = $(this);
            var newName   = span.text().trim();
            var prevName  = span.attr("data-gf-prev-value") || "";
            var group     = span.closest(".gf-dropdown-option-group");
            var fieldWrap = span.closest(".gf-dropdown-field-wrap");

            if (newName === "") {
                span.text(prevName);
                return;
            }

            span.attr("data-gf-prev-value", newName);
            group.attr("data-gf-group-key", newName);
            group.find(".gf-choice-field-add-option-btn").attr("data-gf-group-key", newName);

            gfSaveFieldOptions(fieldWrap);
        });

        $(document).on("keydown", ".gf-dropdown-group-name", function(e) {
            if (e.key === "Enter") {
                e.preventDefault();
                $(this).trigger("blur");
            }
            if (e.key === "Escape") {
                var span = $(this);
                span.text(span.attr("data-gf-prev-value") || "");
                span[0].blur();
            }
        });

        // ─── Optgroup: add group ──────────────────────────────────────────────
        $(document).on("click", ".gf-dropdown-add-group-btn", function(e) {
            e.stopPropagation();
            var fieldWrap = $(this).closest(".gf-dropdown-field-wrap");
            var panel     = fieldWrap.find(".gf-dropdown-options-panel");

            // Determine next group name
            var existing  = panel.find(".gf-dropdown-option-group[data-gf-group-key!=\"\"]").length;
            var groupName = gfDropdownI18n.newGroupLabel + " " + (existing + 1);

            // Switch to grouped mode
            panel.attr("data-gf-grouped", "true");

            // Build new group element
            var newGroup = $("<div>").addClass("gf-dropdown-option-group").attr("data-gf-group-key", groupName);

            // Editable group header
            var header = $("<div>").addClass("gf-dropdown-group-header");
            var nameSpan = $("<span>").addClass("gf-dropdown-group-name")
                .attr({ contenteditable: "true", spellcheck: "false", "data-gf-prev-value": groupName })
                .text(groupName);
            var toolbar = $("<div>").addClass("gf-sub-toolbar gf-dropdown-group-toolbar")
                .attr("role", "group");
            var deleteBtn = $("<button>").attr({ type: "button", "aria-label": gfDropdownI18n.ariaDeleteGroup })
                .addClass("gf-sub-toolbar-btn gf-dropdown-group-delete")
                .text("✕");
            toolbar.append(deleteBtn);
            header.append(nameSpan).append(toolbar);
            newGroup.append(header);

            // Starter option row — clone from existing or build fresh
            var prefix2   = gfDropdownI18n.newOptionLabel + " ";
            var maxN2     = 0;
            fieldWrap.find(".gf-dropdown-option-group .gf-choice-option-row").each(function() {
                var lbl = $(this).attr("data-gf-option-label") || "";
                if (lbl.indexOf(prefix2) === 0) {
                    var n = parseInt(lbl.slice(prefix2.length), 10);
                    if (!isNaN(n) && n > maxN2) { maxN2 = n; }
                }
            });
            var newLabel  = prefix2 + (maxN2 + 1);
            var newValue  = gfDeriveOptionValue(newLabel);
            var template  = fieldWrap.find(".gf-choice-option-row").first();
            var starterRow;
            if (template.length) {
                starterRow = template.clone(false);
                starterRow.attr("data-gf-option-label", newLabel);
                starterRow.attr("data-gf-option-value", newValue);
                starterRow.removeClass("gf-choice-option-row--focused");
                starterRow.find(".gf-choice-option-label").text(newLabel).attr("data-gf-prev-value", newLabel);
                starterRow.find(".gf-choice-option-value-input").val(newValue);
                starterRow.find(".gf-choice-option-value-popover").prop("hidden", true);
                starterRow.find(".gf-choice-option-toolbar").removeClass("gf-sub-toolbar--visible");
                starterRow.find(".gf-choice-option-delete").prop("disabled", false);
            } else {
                starterRow = $("<div>").addClass("gf-choice-option-row text-start w-100 pb-2")
                    .attr({ "data-gf-option-label": newLabel, "data-gf-option-value": newValue });
            }
            newGroup.append(starterRow);

            // Add-option button for new group
            var addWrap = $("<div>").addClass("gf-choice-field-add-option-wrap");
            var addBar  = $("<div>").addClass("gf-choice-field-add-option-bar");
            var addBtn  = $("<button>").attr({ type: "button", "class": "gf-choice-field-add-option-btn", "aria-label": gfDropdownI18n.ariaAddOption, "data-gf-group-key": groupName })
                .append($("<span>").addClass("gf-choice-add-option-icon").text("+"));
            addWrap.append(addBar).append(addBtn);
            newGroup.append(addWrap);

            // Insert before add-group wrap
            fieldWrap.find(".gf-dropdown-add-group-wrap").before(newGroup);

            gfRefreshDropdownDeleteGuards(fieldWrap);
            gfInitDropdownSortable(fieldWrap);

            gfSaveFieldOptions(fieldWrap, function() {
                nameSpan.focus();
                // Place cursor at end
                var range = document.createRange();
                var sel   = window.getSelection();
                range.selectNodeContents(nameSpan[0]);
                range.collapse(false);
                sel.removeAllRanges();
                sel.addRange(range);
            });
        });

        // ─── Optgroup: delete group ───────────────────────────────────────────
        $(document).on("click", ".gf-dropdown-group-delete", function(e) {
            e.stopPropagation();
            var group     = $(this).closest(".gf-dropdown-option-group");
            var fieldWrap = $(this).closest(".gf-dropdown-field-wrap");
            var panel     = fieldWrap.find(".gf-dropdown-options-panel");

            // Rescue all option rows to the "" (ungrouped) group
            var ungrouped = panel.find(".gf-dropdown-option-group[data-gf-group-key=\"\"]");
            var addWrap   = ungrouped.find(".gf-choice-field-add-option-wrap");
            group.find(".gf-choice-option-row").each(function() {
                addWrap.before($(this).detach());
            });

            group.remove();

            // If no named groups remain, switch back to simple mode
            var hasNamed = panel.find(".gf-dropdown-option-group").filter(function() {
                return $(this).attr("data-gf-group-key") !== "";
            }).length > 0;
            if (!hasNamed) {
                panel.attr("data-gf-grouped", "false");
            }

            gfRefreshDropdownDeleteGuards(fieldWrap);
            gfSaveFieldOptions(fieldWrap);
        });

        // ─── Drag/sort (dropdown) ─────────────────────────────────────────────
        function gfInitDropdownSortable(fieldWrap) {
            var allGroups = fieldWrap.find(".gf-dropdown-option-group");

            // Destroy any existing sortable instances on these groups
            allGroups.each(function() {
                if ($(this).is(".ui-sortable")) {
                    $(this).sortable("destroy");
                }
            });

            allGroups.sortable({
                items: ".gf-choice-option-row",
                handle: ".gf-choice-option-drag-handle",
                cancel: "input, textarea, select, option, button:not(.gf-choice-option-drag-handle)",
                connectWith: allGroups,
                axis: false,
                tolerance: "pointer",
                helper: "clone",
                start: function(event, ui) {
                    gfDeselectAllOptionRows(fieldWrap);
                    fieldWrap.addClass("gf-choice-field-wrap--dragging");
                },
                sort: function(event, ui) {
                    var addWrap = $(this).find("> .gf-choice-field-add-option-wrap");
                    if (addWrap.length && ui.placeholder.index() > addWrap.index()) {
                        addWrap.before(ui.placeholder);
                    }
                },
                stop: function(event, ui) {
                    fieldWrap.removeClass("gf-choice-field-wrap--dragging");

                    // Ensure dropped item sits above the add-option wrap only if it landed past it
                    var addWrap = ui.item.closest(".gf-dropdown-option-group").find(".gf-choice-field-add-option-wrap");
                    if (addWrap.length && ui.item.index() > addWrap.index()) {
                        addWrap.before(ui.item);
                    }

                    // Remove empty named groups
                    fieldWrap.find(".gf-dropdown-option-group").each(function() {
                        var key = $(this).attr("data-gf-group-key");
                        if (key !== "" && $(this).find(".gf-choice-option-row").length === 0) {
                            $(this).remove();
                        }
                    });

                    // Check grouped state
                    var panel    = fieldWrap.find(".gf-dropdown-options-panel");
                    var hasNamed = panel.find(".gf-dropdown-option-group").filter(function() {
                        return $(this).attr("data-gf-group-key") !== "";
                    }).length > 0;
                    panel.attr("data-gf-grouped", hasNamed ? "true" : "false");

                    gfRefreshDropdownDeleteGuards(fieldWrap);
                    gfSaveFieldOptions(fieldWrap);
                    gfInitDropdownSortable(fieldWrap);

                    ui.item.addClass("gf-choice-option-row--landing");
                    ui.item.one("animationend", function() {
                        $(this).removeClass("gf-choice-option-row--landing");
                    });
                }
            });
        }

        document.addEventListener("form-html-received", function() {
            requestAnimationFrame(function() {
                // Dropdown panels start closed; sortable will init on panel open
            });
        });

        ';
    }

    /**
     * All JS for inline payment field product editing (inline contenteditable + image + Add Product canvas).
     */
    private function paymentFieldInitJs()
    {
        $form_id = isset($_GET['form_id']) ? absint($_GET['form_id']) : 0;
        echo '
        // ─── Payment field inline product editing ──────────────────────────────────

        var gfPaymentMediaTitle      = ' . json_encode(__('Select product image', 'griffinforms-form-builder')) . ';
        var gfPaymentMediaButtonText = ' . json_encode(__('Use image', 'griffinforms-form-builder')) . ';


        // Collect all products from a payment field DOM into an array of objects
        function gfCollectPaymentProducts(fieldWrap) {
            var products = [];
            fieldWrap.find(".gf-preview-item[data-gf-product-id]").each(function() {
                var $card    = $(this);
                var name     = $card.find(".gf-payment-product-name-editable").text().trim();
                var rawPrice = $card.find(".gf-payment-product-price-editable").data("gf-raw-price");
                var price    = parseFloat(rawPrice) || 0;
                var desc     = $card.find(".gf-payment-product-desc-editable").text().trim();
                var image    = $card.find(".gf-payment-image-wrap img").attr("src") || "";
                products.push({
                    id:          $card.data("gf-product-id") || "",
                    name:        name,
                    price:       price,
                    description: desc,
                    image:       image,
                    type:        "simple"
                });
            });
            return products;
        }

        // Resolve the fieldWrap ([data-gf-itemid] ancestor) from any element inside a payment preview
        function gfGetPaymentFieldWrap($el) {
            return $el.closest("[data-gf-itemid][data-gf-fieldtype]");
        }

        // Resolve the payment preview div from any element inside it
        function gfGetPaymentPreview($el) {
            return $el.closest(".griffinforms-payment-preview");
        }

        // Save all products for a payment field; optionally run a callback on success
        function gfSavePaymentProducts($fieldWrap, onSuccess) {
            var fieldId   = $fieldWrap.attr("data-gf-itemid");
            var fieldType = $fieldWrap.attr("data-gf-fieldtype") || "";
            if (!fieldId) { return; }

            var products = gfCollectPaymentProducts($fieldWrap);
            statusbarMsg("waiting");

            $.post(ajaxurl, {
                action:        "updateFieldOptions",
                field_id:      fieldId,
                field_type:    fieldType,
                field_options: JSON.stringify(products),
                nonce:         gfChoiceNonce
            }, function(response) {
                response = JSON.parse(response);
                if (response.success) {
                    statusbarMsg("success", response.message + " @ " + response.time);
                    if (typeof onSuccess === "function") { onSuccess(); }
                } else {
                    statusbarMsg("warning", response.message);
                }
            });
        }

        // Refresh the field fragment from server after adding a new product
        function gfRefreshPaymentField($fieldWrap) {
            var itemId = $fieldWrap.attr("data-gf-itemid");
            if (!itemId) { return; }
            $.post(ajaxurl, {
                action:    "getFormElementHtml",
                item_type: "field",
                item_id:   itemId,
                parent_id: 0,
                form_id:   ' . esc_js($form_id) . '
            }, function(html) {
                var $newField = $(html);
                $fieldWrap.replaceWith($newField);
                selectFormElement($newField);
                document.dispatchEvent(formHtmlReceived);
            });
        }

        // ── Inline name editing ────────────────────────────────────────────────
        $(document).on("blur", ".gf-payment-product-name-editable", function() {
            var $el     = $(this);
            var newVal  = $el.text().trim();
            var prevVal = $el.data("gf-prev-value");
            if (newVal === "") { $el.text(prevVal); return; }
            if (newVal === prevVal) { return; }
            $el.data("gf-prev-value", newVal);
            var $fw = gfGetPaymentFieldWrap($el);
            if ($fw.length) { gfSavePaymentProducts($fw); }
        });

        // ── Inline price editing ───────────────────────────────────────────────
        // Strip anything that is not a digit or a decimal point on every keystroke.
        // Also enforce a single decimal point so the user cannot type "1..2" or "1.2.3".
        $(document).on("keydown", ".gf-payment-product-price-editable", function(e) {
            var allowed = e.key === "Backspace" || e.key === "Delete" ||
                          e.key === "ArrowLeft" || e.key === "ArrowRight" ||
                          e.key === "Tab" || e.key === "Enter" || e.key === "Escape" ||
                          e.key === "Home" || e.key === "End";
            if (allowed) { return; }
            // Allow digits
            if (/^[0-9]$/.test(e.key)) { return; }
            // Allow a single decimal point
            if (e.key === ".") {
                if ($(this).text().indexOf(".") !== -1) { e.preventDefault(); }
                return;
            }
            // Block everything else (including currency symbols, letters, etc.)
            e.preventDefault();
        });
        $(document).on("input", ".gf-payment-product-price-editable", function() {
            // Sanitize in case text was pasted
            var $el  = $(this);
            var raw  = $el.text().replace(/[^0-9.]/g, "");
            // Remove extra decimal points — keep only the first
            var parts = raw.split(".");
            if (parts.length > 2) { raw = parts[0] + "." + parts.slice(1).join(""); }
            $el.data("gf-raw-price", raw);
        });
        $(document).on("blur", ".gf-payment-product-price-editable", function() {
            var $el     = $(this);
            var rawVal  = $el.data("gf-raw-price") || $el.text().replace(/[^0-9.]/g, "");
            var price   = parseFloat(rawVal) || 0;
            var formatted = price.toFixed(2);
            var prevRaw = $el.data("gf-prev-value");
            // Update display to canonical formatted number
            $el.text(formatted);
            $el.data("gf-raw-price", formatted);
            if (formatted === prevRaw) { return; }
            $el.data("gf-prev-value", formatted);
            var $fw = gfGetPaymentFieldWrap($el);
            if ($fw.length) { gfSavePaymentProducts($fw); }
        });

        // ── Inline description editing ─────────────────────────────────────────
        $(document).on("blur", ".gf-payment-product-desc-editable", function() {
            var $el     = $(this);
            var newVal  = $el.text().trim();
            var prevVal = $el.data("gf-prev-value");
            if (newVal === prevVal) { return; }
            $el.data("gf-prev-value", newVal);
            $el.toggleClass("gf-payment-product-desc-empty", newVal === "");
            var $fw = gfGetPaymentFieldWrap($el);
            if ($fw.length) { gfSavePaymentProducts($fw); }
        });

        // ── Image: Add Image button ────────────────────────────────────────────
        // Build image wrap / placeholder HTML with correct stack modifier classes
        function gfPaymentImageWrapHtml(url, isStack) {
            var stackClass = isStack ? " gf-preview-media--stack" : "";
            return "<div class=\"gf-preview-media" + stackClass + " gf-payment-image-wrap\">" +
                   "<img src=\"" + url + "\" alt=\"\">" +
                   "</div>";
        }
        function gfPaymentPlaceholderHtml(isStack) {
            var stackClass = isStack ? " gf-preview-media--stack" : "";
            return "<div class=\"gf-preview-media gf-preview-media--placeholder" + stackClass + " gf-payment-image-placeholder\">" +
                   "<button type=\"button\" class=\"gf-payment-add-image-btn gf-btn gf-btn-sm btn-outline-secondary\">' . esc_js(__('Add Image', 'griffinforms-form-builder')) . '</button>" +
                   "</div>";
        }

        $(document).on("click", ".gf-payment-add-image-btn", function(e) {
            e.preventDefault();
            var $placeholder = $(this).closest(".gf-payment-image-placeholder");
            var $card        = $placeholder.closest(".gf-preview-item");
            var isStack      = $card.hasClass("gf-preview-item--stack");
            if (typeof wp === "undefined" || !wp.media) { return; }

            var frame = wp.media({
                title:    gfPaymentMediaTitle,
                button:   { text: gfPaymentMediaButtonText },
                multiple: false
            });
            frame.on("select", function() {
                var url = frame.state().get("selection").first().toJSON().url;
                if (!url) { return; }
                $placeholder.replaceWith(gfPaymentImageWrapHtml(url, isStack));
                var $fw = gfGetPaymentFieldWrap($card);
                if ($fw.length) {
                    gfRefreshProductToolbarGuards($fw);
                    gfSavePaymentProducts($fw);
                }
                frame.close();
            });
            frame.open();
        });

        // ── Add Product: browse image in canvas ────────────────────────────────
        $(document).on("click", ".gf-payment-canvas-image-pick", function(e) {
            e.preventDefault();
            console.log("[gf-payment] canvas-image-pick click fired");
            var $input = $(this).closest(".input-group").find(".gf-payment-canvas-image");
            console.log("[gf-payment] wp defined:", typeof wp !== "undefined", "| wp.media defined:", typeof wp !== "undefined" && !!wp.media);
            if (typeof wp === "undefined" || !wp.media) {
                console.warn("[gf-payment] wp.media not available — aborting");
                return;
            }
            var frame = wp.media({ title: gfPaymentMediaTitle, button: { text: gfPaymentMediaButtonText }, multiple: false });
            console.log("[gf-payment] canvas frame created:", frame);
            frame.on("select", function() {
                var url = frame.state().get("selection").first().toJSON().url;
                if (url) { $input.val(url); }
                frame.close();
            });
            console.log("[gf-payment] calling canvas frame.open()");
            frame.open();
            console.log("[gf-payment] canvas frame.open() returned");
        });

        // ── Add Product: save new product ──────────────────────────────────────
        $(document).on("click", ".gf-payment-save-product-btn", function() {
            var $canvas   = $(this).closest(".gf-payment-product-canvas");
            var $fieldWrap = $canvas.closest("[data-gf-itemid][data-gf-fieldtype]");
            var name       = $canvas.find(".gf-payment-canvas-name").val().trim();
            if (!name) { $canvas.find(".gf-payment-canvas-name").focus(); return; }

            var fieldId   = $fieldWrap.attr("data-gf-itemid");
            var fieldType = $fieldWrap.attr("data-gf-fieldtype") || "";
            if (!fieldId) { return; }

            var newProduct = {
                id:          "prod_" + Math.random().toString(36).substr(2, 6),
                name:        name,
                price:       parseFloat($canvas.find(".gf-payment-canvas-price").val()) || 0,
                description: $canvas.find(".gf-payment-canvas-desc").val().trim(),
                image:       $canvas.find(".gf-payment-canvas-image").val().trim(),
                type:        "simple"
            };

            var existing = gfCollectPaymentProducts($fieldWrap);
            var products  = existing.concat([newProduct]);

            statusbarMsg("waiting");
            $canvas.attr("hidden", "").find("input, textarea").val("");

            $.post(ajaxurl, {
                action:        "updateFieldOptions",
                field_id:      fieldId,
                field_type:    fieldType,
                field_options: JSON.stringify(products),
                nonce:         gfChoiceNonce
            }, function(response) {
                response = JSON.parse(response);
                if (response.success) {
                    statusbarMsg("success", response.message + " @ " + response.time);
                    gfRefreshPaymentField($fieldWrap);
                } else {
                    statusbarMsg("warning", response.message);
                    $canvas.removeAttr("hidden");
                }
            });
        });

        // ── Add Product: cancel ────────────────────────────────────────────────
        $(document).on("click", ".gf-payment-cancel-product-btn", function() {
            $(this).closest(".gf-payment-product-canvas").attr("hidden", "").find("input, textarea").val("");
        });

        // ─── Product card sub-toolbar ──────────────────────────────────────────

        // Select / deselect product cards (show sub-toolbar)
        $(document).on("mousedown", ".gf-payment-product-card", function(e) {
            // Sub-toolbar buttons must not re-select (they handle their own events)
            if ($(e.target).closest(".gf-sub-toolbar").length) {
                return;
            }
            // Drag handle is handled by sortable — skip
            if ($(e.target).closest(".gf-payment-product-drag-handle").length) {
                return;
            }
            e.stopPropagation();
            gfSelectPaymentCard($(this));
        });

        function gfSelectPaymentCard($card) {
            $(".gf-payment-product-card--focused").not($card).removeClass("gf-payment-product-card--focused");
            $card.addClass("gf-payment-product-card--focused");
        }

        // Deselect on outside click
        $(document).on("mousedown.gf-payment-cards", function(e) {
            if (!$(e.target).closest(".gf-payment-product-card, .gf-payment-product-toolbar").length) {
                $(".gf-payment-product-card--focused").removeClass("gf-payment-product-card--focused");
            }
        });

        // Deselect cards and close canvas when builder selection changes
        function gfClosePaymentCanvas() {
            $(".gf-payment-product-canvas:not([hidden])").attr("hidden", "").find("input, textarea").val("");
        }
        document.addEventListener("item-selected", function() {
            $(".gf-payment-product-card--focused").removeClass("gf-payment-product-card--focused");
            gfClosePaymentCanvas();
        });
        document.addEventListener("item-deselected", function() {
            gfClosePaymentCanvas();
        });

        // Refresh product sub-toolbar guards (disabled states) after DOM changes
        function gfRefreshProductToolbarGuards($fieldWrap) {
            var $cards = $fieldWrap.find(".gf-payment-product-card");
            var total  = $cards.length;
            $cards.each(function(idx) {
                var $card = $(this);
                var $tb   = $card.find(".gf-payment-product-toolbar");
                $tb.find(".gf-payment-product-move-prev").prop("disabled", idx === 0);
                $tb.find(".gf-payment-product-move-next").prop("disabled", idx === total - 1);
                $tb.find(".gf-payment-product-delete-btn").prop("disabled", total === 1);
                // Image remove: disabled when card has no image wrap
                var $hasImage = $card.find(".gf-payment-image-wrap").length > 0;
                $tb.find(".gf-payment-product-remove-image-btn").prop("disabled", !$hasImage);
            });
        }

        // ── Product card: move prev / next ─────────────────────────────────────
        $(document).on("click", ".gf-payment-product-move-prev", function(e) {
            e.preventDefault();
            var $card    = $(this).closest(".gf-payment-product-card");
            var $grid    = $card.parent();
            var $prev    = $card.prev(".gf-payment-product-card");
            if (!$prev.length) { return; }
            $prev.before($card);
            gfSelectPaymentCard($card);
            var $fw = gfGetPaymentFieldWrap($card);
            if ($fw.length) {
                gfRefreshProductToolbarGuards($fw);
                gfSavePaymentProducts($fw);
            }
        });

        $(document).on("click", ".gf-payment-product-move-next", function(e) {
            e.preventDefault();
            var $card = $(this).closest(".gf-payment-product-card");
            var $next = $card.next(".gf-payment-product-card");
            if (!$next.length) { return; }
            $next.after($card);
            gfSelectPaymentCard($card);
            var $fw = gfGetPaymentFieldWrap($card);
            if ($fw.length) {
                gfRefreshProductToolbarGuards($fw);
                gfSavePaymentProducts($fw);
            }
        });

        // ── Product card: delete ───────────────────────────────────────────────
        $(document).on("click", ".gf-payment-product-delete-btn", function(e) {
            e.preventDefault();
            var $card = $(this).closest(".gf-payment-product-card");
            var $fw   = gfGetPaymentFieldWrap($card);
            $card.remove();
            if ($fw.length) {
                gfRefreshProductToolbarGuards($fw);
                gfSavePaymentProducts($fw);
            }
        });

        // ── Product card: add / replace image (sub-toolbar) ───────────────────
        $(document).on("click", ".gf-payment-product-image-btn", function(e) {
            e.preventDefault();
            var $card   = $(this).closest(".gf-payment-product-card");
            var isStack = $card.hasClass("gf-preview-item--stack");
            if (typeof wp === "undefined" || !wp.media) { return; }
            var frame = wp.media({ title: gfPaymentMediaTitle, button: { text: gfPaymentMediaButtonText }, multiple: false });
            frame.on("select", function() {
                var url = frame.state().get("selection").first().toJSON().url;
                if (!url) { return; }
                var $existing    = $card.find(".gf-payment-image-wrap");
                var $placeholder = $card.find(".gf-payment-image-placeholder");
                var imageHtml    = gfPaymentImageWrapHtml(url, isStack);
                if ($existing.length) {
                    $existing.replaceWith(imageHtml);
                } else if ($placeholder.length) {
                    $placeholder.replaceWith(imageHtml);
                } else if (isStack) {
                    // Stack: image must be first child inside the flex wrapper
                    $card.find(".d-flex.align-items-start").prepend(imageHtml);
                } else {
                    $card.prepend(imageHtml);
                }
                var $fw = gfGetPaymentFieldWrap($card);
                if ($fw.length) {
                    gfRefreshProductToolbarGuards($fw);
                    gfSavePaymentProducts($fw);
                }
                frame.close();
            });
            frame.open();
        });

        // ── Product card: remove image (sub-toolbar) ───────────────────────────
        $(document).on("click", ".gf-payment-product-remove-image-btn", function(e) {
            e.preventDefault();
            var $card   = $(this).closest(".gf-payment-product-card");
            var isStack = $card.hasClass("gf-preview-item--stack");
            $card.find(".gf-payment-image-wrap").replaceWith(gfPaymentPlaceholderHtml(isStack));
            var $fw = gfGetPaymentFieldWrap($card);
            if ($fw.length) {
                gfRefreshProductToolbarGuards($fw);
                gfSavePaymentProducts($fw);
            }
        });

        // ── Product card: jQuery UI sortable ───────────────────────────────────
        function gfInitProductSortable($fieldWrap) {
            var $grid = $fieldWrap.find(".gf-preview-grid, .gf-preview-stack");
            if (!$grid.length) { return; }
            $grid.sortable({
                items:     ".gf-payment-product-card",
                handle:    ".gf-payment-product-drag-handle",
                cancel:    "input, textarea, button:not(.gf-payment-product-drag-handle), [contenteditable]",
                tolerance: "pointer",
                start: function(ev, ui) {
                    $(".gf-payment-product-card--focused").removeClass("gf-payment-product-card--focused");
                    ui.item.addClass("gf-payment-product-card--dragging");
                },
                stop: function(ev, ui) {
                    ui.item.removeClass("gf-payment-product-card--dragging");
                    gfRefreshProductToolbarGuards($fieldWrap);
                    gfSavePaymentProducts($fieldWrap);
                }
            });
        }

        // Init sortable on all payment fields on load and after DOM refresh
        $(".griffinforms-payment-preview").each(function() {
            gfInitProductSortable(gfGetPaymentFieldWrap($(this)));
        });

        document.addEventListener("form-html-received", function() {
            $(".griffinforms-payment-preview").each(function() {
                var $fw = gfGetPaymentFieldWrap($(this));
                if ($fw.length) {
                    gfInitProductSortable($fw);
                    gfRefreshProductToolbarGuards($fw);
                }
            });
        });

        ';
    }

    /**
     * Adds JavaScript to update page indexes and navigation buttons after reordering pages.
     */
    private function updatePageUiJs()
    {
        echo '
            function refreshPageUI() {
                $(".griffinforms-app-formlayout-formpage").each(function(index) {
                    refreshPageIndexes($(this), index);
                    refreshPageBtns($(this), index, $(".griffinforms-app-formlayout-formpage").length);
                });
            }

            function refreshPageIndexes(page, index) {
                page.attr("data-gf-index", index);
                // Update the visible index label inside the page header to reflect the current page position
                const indexLabel = page.find(".gf-formpage-id-label .gf-formpage-index");
                if (indexLabel.length) {
                    indexLabel.text(index + 1);
                }
            }

            function refreshPageBtns(page, index, totalPages) {
                const navBtns = page.find(".griffinforms-app-formlayout-pagenav-btns");
                const prevBtn = page.find(".gf-page-prev-btn");
                const nextBtn = page.find(".gf-page-next-btn");
                const submitBtn = page.find(".gf-page-submit-btn");

                prevBtn.addClass("visually-hidden");
                nextBtn.addClass("visually-hidden");
                submitBtn.addClass("visually-hidden");

                const isFirst = index === 0;
                const isLast = index === totalPages - 1;
                const isSingle = totalPages === 1;

                const btnContainerBaseClass = "griffinforms-app-formlayout-pagenav-btns d-flex mt-4 p-2 rounded-2";
                if (isSingle) {
                    submitBtn.removeClass("visually-hidden");
                    navBtns.attr("class", btnContainerBaseClass + " justify-content-end");
                } else if (isFirst) {
                    nextBtn.removeClass("visually-hidden");
                    navBtns.attr("class", btnContainerBaseClass + " justify-content-end");
                } else if (isLast) {
                    prevBtn.removeClass("visually-hidden");
                    submitBtn.removeClass("visually-hidden");
                    navBtns.attr("class", btnContainerBaseClass + " justify-content-between");
                } else {
                    prevBtn.removeClass("visually-hidden");
                    nextBtn.removeClass("visually-hidden");
                    navBtns.attr("class", btnContainerBaseClass + " justify-content-between");
                }
            }

            $(document).on("form-html-received", refreshPageUI);
            ';
    }

    private function getFormLayoutJs()
    {
        $form_layout_nonce = wp_create_nonce('form_layout');
        $form_id = false;
        $mode = isset($_GET['mode']) ? sanitize_key(wp_unslash($_GET['mode'])) : '';

        if (!empty($_GET['form_id'])) {
            $form_id = absint($_GET['form_id']);
        }

        echo '
        document.addEventListener("form-layout", () => {
            let formId = ' . absint($form_id) . ';

            if (!formId) {
                formId = selectedForms[0];
            }

            if (formId) {
                try {
                    if (window.sessionStorage) {
                        window.sessionStorage.setItem("gfFormId", formId);
                    }
                } catch (e) {}
            }

            getFormLayoutHtml(formId);
            
        });';

        echo 
        'function getFormLayoutHtml(formId) {

            const data = {
                "action": "getFormLayoutHtml",
                "form_id": formId,
                "nonce": "' . esc_js($form_layout_nonce) . '",
                "mode": "' . esc_js($mode) . '"
            };

            $.post(ajaxurl, data, function(response) {
                $("#' . esc_attr($this->getHtmlId()) . '").html(response);
                document.dispatchEvent(formHtmlReceived);
            }).fail(function(jqXHR, textStatus) {
                statusbarMsg("danger", "Network error: " + textStatus);
            });
        }' . PHP_EOL;
    }

    private function aiDraftBannerJs()
    {
        echo '
        $("#' . esc_attr($this->getHtmlId()) . '").on("click", "[data-gf-ai-draft-action]", function(event) {
            event.preventDefault();

            const button = $(this);
            const banner = button.closest("[data-gf-ai-draft-banner=true]");
            if (!banner.length) {
                return;
            }

            const action = button.attr("data-gf-ai-draft-action");
            const formId = parseInt(banner.attr("data-gf-form-id"), 10) || 0;
            const isDiscard = action === "discard";
            const nonce = isDiscard ? banner.attr("data-gf-discard-nonce") : banner.attr("data-gf-publish-nonce");

            if (!formId || !nonce) {
                return;
            }

            if (isDiscard && !window.confirm("' . esc_js(__('Discard this AI draft permanently?', 'griffinforms-form-builder')) . '")) {
                return;
            }

            const buttons = banner.find("button");
            buttons.prop("disabled", true);

            $.post(ajaxurl, {
                action: isDiscard ? "discard_ai_draft" : "publish_ai_draft",
                form_id: formId,
                nonce: nonce
            }, function(response) {
                if (response && response.success && response.data && response.data.redirect_url) {
                    window.location.href = response.data.redirect_url;
                    return;
                }

                const message = response && response.data && response.data.message
                    ? response.data.message
                    : "' . esc_js(__('Draft action failed. Please try again.', 'griffinforms-form-builder')) . '";

                if (response && response.data && response.data.status === "invalid_state") {
                    window.location.href = "' . esc_js(admin_url('admin.php?page=griffinforms-app&form_id=')) . '" + formId;
                    return;
                }

                alert(message);
                buttons.prop("disabled", false);
            }).fail(function() {
                alert("' . esc_js(__('Network error while resolving this AI draft. Please try again.', 'griffinforms-form-builder')) . '");
                buttons.prop("disabled", false);
            });
        });
        ' . PHP_EOL;
    }

    /**
     * Adds visual cue to selected form elements like page, row etc.
     * The element must have [data-gf-fe="true"] data attribute for this to work.
     * @see FormElements\Format::dataAttrs()
     */
    private function formElementClickJs()
    {
        echo '
        // Global variables for tracking the currently selected item
        var currentSelectedItemId = null;
        var currentSelectedItemType = null;

        $("#' . esc_attr($this->getHtmlId()) . '").on("click", function(event) {
            const target = $(event.target);
            const formRoot = target.closest(".griffinforms-app-formlayout-form");

            if (
                target.closest(".gf-fe-action-cluster").length > 0 ||
                target.closest("[data-gf-action]").length > 0
            ) {
                return;
            }

            if (formRoot.length > 0 && target.closest(".gf-builder-form-meta-shell").length > 0) {
                return;
            }

            const container = target.closest("[data-gf-fe=true]");

            if (container.length > 0) {
                // Only select if it is not already selected.
                if (container.attr("data-gf-selected") !== "true") {
                    selectFormElement(container);
                }
            } else {
                deselectAllFormElements();
            }
            
        });
        
        const rowBadgeTemplate = "' . esc_js(__('Row %1$d of Page %2$d', 'griffinforms-form-builder')) . '";
        const pageBadgeTemplate = "' . esc_js(__('Page %d', 'griffinforms-form-builder')) . '";

        function updateToolbarMeta(element, metaText) {
            const metaNode = $(element).children(".gf-fe-action-cluster").find(".gf-fe-action-meta").first();
            if (!metaNode.length) {
                return;
            }

            const cleanText = (metaText || "").trim();
            metaNode.text(cleanText);
            metaNode.toggleClass("d-none", cleanText === "");
        }

        function getBuilderMetadataEditor(surface) {
            return $(surface).find(".gf-builder-metadata-editor").first();
        }

        function syncBuilderMetadataSurface(surface) {
            surface = $(surface);
            if (!surface.length) {
                return;
            }

            const editor = getBuilderMetadataEditor(surface);
            if (!editor.length) {
                return;
            }

            const isInputActive = document.activeElement === editor.get(0);
            const rawValue = editor.is("textarea, input") ? editor.val() : editor.text();
            const isEmpty = $.trim(rawValue || "") === "";

            surface.attr("data-gf-empty", isEmpty ? "true" : "false");
        }

        function setMenuActionDisabled(actionBtn, disabled) {
            const shouldDisable = !!disabled;
            if (!actionBtn || !actionBtn.length) {
                return;
            }

            if (actionBtn.is("button")) {
                actionBtn.prop("disabled", shouldDisable);
            }

            actionBtn.attr("aria-disabled", shouldDisable ? "true" : "false");
            actionBtn.toggleClass("is-disabled", shouldDisable);
        }

        function refreshMinimumStructureDeleteStates() {
            $(".griffinforms-app-formlayout-formpage").each(function() {
                const page = $(this);
                const pageDeleteBtn = page.find(".gf-fe-action-menu-item[data-gf-action=delete-selected-item][data-gf-context-type=page]").first();
                if (pageDeleteBtn.length) {
                    const pageCount = $(".griffinforms-app-formlayout-formpage-container").length;
                    setMenuActionDisabled(pageDeleteBtn, pageCount <= 1);
                }
            });

            $(".griffinforms-app-formlayout-pagerow").each(function() {
                const row = $(this);
                const rowDeleteBtn = row.find(".gf-fe-action-menu-item[data-gf-action=delete-selected-item][data-gf-context-type=row]").first();
                if (rowDeleteBtn.length) {
                    const rowCount = row.closest(".griffinforms-app-formlayout-formpage-rows-container").children(".griffinforms-app-formlayout-pagerow").length;
                    setMenuActionDisabled(rowDeleteBtn, rowCount <= 1);
                }
            });

            $(".griffinforms-app-formlayout-rowcolumn").each(function() {
                const col = $(this);
                const colDeleteBtn = col.find(".gf-fe-action-menu-item[data-gf-action=delete-selected-item][data-gf-context-type=column]").first();
                if (colDeleteBtn.length) {
                    const colCount = col.parent().children(".griffinforms-app-formlayout-rowcolumn").length;
                    setMenuActionDisabled(colDeleteBtn, colCount <= 1);
                }
            });
        }

        document.addEventListener("form-html-received", refreshMinimumStructureDeleteStates);

        function deselectAllFormElements() {
            const lastItemId = currentSelectedItemId || null;
            const lastItemType = currentSelectedItemType || null;

            $("[data-gf-fe=true]").each(function(){
                $(this).attr("data-gf-selected", "false");
                $(this).removeAttr("data-gf-selected-child");
                $(this).removeAttr("data-gf-metadata-visible");
                $(this).removeClass("gf-fe-active-hover");
                updateToolbarMeta($(this), "");
            });
            $(".griffinforms-app-formlayout-formpage-container").attr("data-gf-selected", "false");
            $(".griffinforms-app-formlayout-formpage-container").removeAttr("data-gf-selected-child");
            $(".griffinforms-app-formlayout-form").attr("data-gf-form-selected", "false");
            $(".griffinforms-app-formlayout-form").removeAttr("data-gf-selected-child");
            currentSelectedItemId = null;
            currentSelectedItemType = null;

            const deselectEvent = new CustomEvent("item-deselected", {
                detail: {
                    itemId: null,
                    itemType: null,
                    lastItemId: lastItemId,
                    lastItemType: lastItemType
                }
            });
            document.dispatchEvent(deselectEvent);
        }

        function selectFormRoot(formElement) {
            const form = $(formElement).first();
            if (!form.length) {
                return;
            }

            deselectAllFormElements();
            form.attr("data-gf-form-selected", "true");
        }

        function collapseFormMetadataDisclosure(formElement) {
            const form = $(formElement).first();
            if (!form.length) {
                return;
            }

            form.attr("data-gf-form-selected", "false");
        }
        
        function selectFormElement(element) {
            element = $(element);
            if (!element || element.length === 0) {
                console.error("selectFormElement: Provided element does not exist.");
                return;
            }

            function getPagePosition(pageContainer) {
                if (!pageContainer || !pageContainer.length) return NaN;
                const form = pageContainer.closest(".griffinforms-app-formlayout-form");
                const pages = form.find(".griffinforms-app-formlayout-formpage-container");
                return pages.index(pageContainer) + 1;
            }

            deselectAllFormElements();
            $(element).attr("data-gf-selected", "true");
            const form = $(element).closest(".griffinforms-app-formlayout-form");
            if ($(element).hasClass("griffinforms-app-formlayout-formpage")) {
                $(element).closest(".griffinforms-app-formlayout-formpage-container").attr("data-gf-selected", "true");
                form.attr("data-gf-selected-child", "page");
            }
            // Mark parent column when a field is selected (for outline)
            if ($(element).hasClass("griffinforms-app-formlayout-field")) {
                const col = $(element).closest(".griffinforms-app-formlayout-rowcolumn");
                const row = $(element).closest(".griffinforms-app-formlayout-pagerow");
                const page = $(element).closest(".griffinforms-app-formlayout-formpage");
                col.attr("data-gf-selected-child", "field");
                row.attr("data-gf-selected-child", "field");
                page.attr("data-gf-selected-child", "field");
                form.attr("data-gf-selected-child", "field");
                const ffriendly = $(element).attr("data-gf-fieldfriendly") || $(element).attr("data-gf-fieldtype") || "";
                if (ffriendly) {
                    updateToolbarMeta($(element), ffriendly);
                }
            }
            // Add row position badge when a row is selected
            if ($(element).hasClass("griffinforms-app-formlayout-pagerow")) {
                const page = $(element).closest(".griffinforms-app-formlayout-formpage");
                page.attr("data-gf-selected-child", "row");
                form.attr("data-gf-selected-child", "row");
                const pageContainer = $(element).closest(".griffinforms-app-formlayout-formpage-container");
                const pageIndex = getPagePosition(pageContainer);
                const rows = pageContainer.find(".griffinforms-app-formlayout-pagerow");
                const rowIndex = rows.index(element) + 1;
                if (!isNaN(pageIndex) && !isNaN(rowIndex)) {
                    const msg = rowBadgeTemplate.replace("%1$d", rowIndex).replace("%2$d", pageIndex);
                    updateToolbarMeta($(element), msg);
                }
            }

            // Add page position badge when a page is selected
            if ($(element).hasClass("griffinforms-app-formlayout-formpage")) {
                const pageContainer = $(element).closest(".griffinforms-app-formlayout-formpage-container");
                const pageIndex = getPagePosition(pageContainer);
                if (!isNaN(pageIndex)) {
                    const pageLabel = pageBadgeTemplate.replace("%d", pageIndex);
                    updateToolbarMeta($(element), pageLabel);
                }
            }

            if ($(element).hasClass("griffinforms-app-formlayout-rowcolumn")) {
                const row = $(element).closest(".griffinforms-app-formlayout-pagerow");
                const page = $(element).closest(".griffinforms-app-formlayout-formpage");
                row.attr("data-gf-selected-child", "column");
                page.attr("data-gf-selected-child", "column");
                form.attr("data-gf-selected-child", "column");
                updateToolbarMeta($(element), "' . esc_js(__('Column', 'griffinforms-form-builder')) . '");
            }

            const itemId = $(element).attr("data-gf-itemid") ?? 0;
            const itemType = $(element).attr("data-gf-itemtype") ?? false;
            currentSelectedItemId = itemId;
            currentSelectedItemType = itemType;
            dispatchItemSelected(itemId, itemType); 
        }

        function dispatchItemSelected(itemId, itemType) {
            const customEvent = new CustomEvent("item-selected", {
                detail: {
                    itemId: itemId,
                    itemType: itemType
                }
            });
            document.dispatchEvent(customEvent);
        }

        $("#' . esc_attr($this->getHtmlId()) . '").on("focusin", ".griffinforms-app-formlayout-form .griffinforms-app-text_update_field[data-gf-fieldname=name]", function() {
            const form = $(this).closest(".griffinforms-app-formlayout-form");
            if (!form.length) {
                return;
            }

            if (form.attr("data-gf-form-selected") !== "true") {
                selectFormRoot(form);
            }
        });

        $("#' . esc_attr($this->getHtmlId()) . '").on("focusout", ".griffinforms-app-formlayout-form .gf-builder-form-meta-shell .griffinforms-app-text_update_field, .griffinforms-app-formlayout-form .gf-builder-form-meta-shell .griffinforms-app-textarea_update_field", function() {
            const form = $(this).closest(".griffinforms-app-formlayout-form");
            if (!form.length) {
                return;
            }

            requestAnimationFrame(function() {
                if (form.find(".gf-builder-form-meta-shell :focus").length) {
                    return;
                }

                if (form.is("[data-gf-selected-child]")) {
                    return;
                }

                collapseFormMetadataDisclosure(form);
            });
        });

        function toggleEmptyMetadataSurface(element) {
            const owner = $(element).closest(".griffinforms-app-formlayout-formpage, .griffinforms-app-formlayout-pagerow");
            if (!owner.length) {
                return;
            }

            const isVisible = owner.attr("data-gf-metadata-visible") === "true";
            if (isVisible) {
                owner.removeAttr("data-gf-metadata-visible");
                return;
            }

            owner.attr("data-gf-metadata-visible", "true");

            requestAnimationFrame(function() {
                const firstEditor = owner
                    .find("[data-gf-structural-metadata=true][data-gf-empty=true] .gf-builder-metadata-editor:visible")
                    .first();

                if (!firstEditor.length) {
                    return;
                }

                firstEditor.trigger("focus");
            });
        }

        function ownerHasEmptyMetadata(owner) {
            const ownerEl = $(owner);
            if (!ownerEl.length) {
                return false;
            }

            let hasEmpty = false;
            ownerEl.find("[data-gf-structural-metadata=true]").each(function() {
                const field = $(this).find(".gf-builder-metadata-editor").first();
                const currentValue = $.trim(field.attr("data-gf-fieldvalue") || field.val() || "");

                if (currentValue === "") {
                    hasEmpty = true;
                    return false;
                }
            });

            return hasEmpty;
        }

        function syncEmptyMetadataToggleButtons(scope) {
            $(scope || document)
                .find(".griffinforms-app-formlayout-formpage, .griffinforms-app-formlayout-pagerow")
                .each(function() {
                    const owner = $(this);
                    const toggleBtn = owner.find(".gf-toggle-empty-metadata-btn").first();

                    if (!toggleBtn.length) {
                        return;
                    }

                    const shouldShow = ownerHasEmptyMetadata(owner);
                    toggleBtn.prop("hidden", !shouldShow);
                    toggleBtn.attr("aria-hidden", shouldShow ? "false" : "true");

                    if (!shouldShow) {
                        owner.removeAttr("data-gf-metadata-visible");
                    }
                });
        }
        ' . PHP_EOL;
    }

    private function formElementHoverJs()
    {
        $delete_selected_item_nonce = wp_create_nonce('delete_selected_item');
        echo '
        const builderRoot = $("#' . esc_attr($this->getHtmlId()) . '");
        let activeToolbarMenuTrigger = null;

        function clearActiveHoverElement() {
            builderRoot.find("[data-gf-fe=true].gf-fe-active-hover").removeClass("gf-fe-active-hover");
        }

        function closeAllToolbarMenus(options = {}) {
            const restoreFocus = !!options.restoreFocus;
            const menuTriggers = builderRoot.find(".gf-fe-action-menu-trigger[aria-expanded=true]");

            menuTriggers.each(function() {
                $(this).attr("aria-expanded", "false");
            });

            builderRoot.find(".gf-fe-action-menu").each(function() {
                const menu = $(this);
                menu.removeClass("is-open");
                window.clearTimeout(menu.data("gfHideTimer"));
                const hideTimer = window.setTimeout(function() {
                    menu.attr("hidden", true);
                }, 160);
                menu.data("gfHideTimer", hideTimer);
            });

            if (restoreFocus && activeToolbarMenuTrigger && activeToolbarMenuTrigger.length) {
                activeToolbarMenuTrigger.trigger("focus");
            }

            activeToolbarMenuTrigger = null;
        }

        function openToolbarMenu(trigger) {
            const menu = trigger.siblings(".gf-fe-action-menu").first();
            if (!menu.length) {
                return;
            }

            closeAllToolbarMenus();
            trigger.attr("aria-expanded", "true");
            menu.attr("hidden", false);
            window.clearTimeout(menu.data("gfHideTimer"));
            requestAnimationFrame(function() {
                menu.addClass("is-open");
            });
            activeToolbarMenuTrigger = trigger;
        }

        function toggleToolbarMenu(trigger) {
            const menu = trigger.siblings(".gf-fe-action-menu").first();
            if (!menu.length) {
                return;
            }

            if (trigger.attr("aria-expanded") === "true") {
                closeAllToolbarMenus({ restoreFocus: true });
                return;
            }

            openToolbarMenu(trigger);
        }

        function getSelectedBuilderItem() {
            let selectedItem = $("[data-gf-selected=true][data-gf-itemtype][data-gf-itemid]").first();
            if (!selectedItem.length) {
                const wrapper = $("[data-gf-selected=true]").first();
                if (wrapper.length) {
                    selectedItem = wrapper.find("[data-gf-itemtype][data-gf-itemid]").first();
                }
            }
            return selectedItem;
        }

        function deleteSelectedBuilderItem() {
            const selectedItem = getSelectedBuilderItem();
            if (!selectedItem.length) {
                return;
            }

            const data = {
                "action": "deleteSelectedItem",
                "item_type": selectedItem.attr("data-gf-itemtype") || "",
                "parent_type": selectedItem.attr("data-gf-parenttype") || "",
                "item_id": selectedItem.attr("data-gf-itemid") || "",
                "parent_id": selectedItem.attr("data-gf-parentid") || "",
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "nonce": "' . esc_js($delete_selected_item_nonce) . '"
            };

            statusbarMsg("waiting");

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);

                if (response.success) {
                    statusbarMsg("success", response.msg);
                    document.dispatchEvent(new CustomEvent("form-updated", { detail: { type: "page_deleted", formId: data.form_id } }));
                    closeAllToolbarMenus();
                    deselectAllFormElements();

                    if (data.item_type === "formpage") {
                        selectedItem.parent().remove();
                    }

                    if (data.item_type === "rowcolumn") {
                        let column = $(".griffinforms-app-formlayout-rowcolumn[data-gf-itemid=" + data.item_id + "]");
                        column.width("0px");
                        toggleCreateRowColumnBtn(column);
                    }

                    selectedItem.remove();
                    document.dispatchEvent(formHtmlReceived);
                } else {
                    statusbarMsg("failure", response.msg);
                }
            }).fail(function(jqXHR, textStatus) {
                statusbarMsg("danger", "Network error: " + textStatus);
            });
        }

        function removeSelectedReusableField() {
            const selectedItem = getSelectedBuilderItem();
            if (!selectedItem.length) {
                return;
            }

            const data = {
                "action": "removeSelectedField",
                "field_id": selectedItem.attr("data-gf-itemid") || "",
                "column_id": selectedItem.closest(".griffinforms-app-formlayout-rowcolumn").attr("data-gf-itemid") || "",
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "nonce": "' . esc_js(wp_create_nonce('remove_selected_field')) . '"
            };

            statusbarMsg("waiting");

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);

                if (response.success) {
                    statusbarMsg("success", response.msg);
                    closeAllToolbarMenus();
                    deselectAllFormElements();
                    selectedItem.remove();
                    document.dispatchEvent(formHtmlReceived);
                } else {
                    statusbarMsg("failure", response.msg);
                }
            }).fail(function(jqXHR, textStatus) {
                statusbarMsg("danger", "Network error: " + textStatus);
            });
        }

        function updateActiveHoverElement(event) {
            clearActiveHoverElement();
            if (!currentSelectedItemId || !currentSelectedItemType) {
                return;
            }

            const selectedElement = builderRoot.find(
                "[data-gf-fe=true][data-gf-itemid=\'" + currentSelectedItemId + "\'][data-gf-itemtype=\'" + currentSelectedItemType + "\']"
            ).first();

            if (!selectedElement.length) {
                return;
            }

            const target = $(event.target);
            const closestSelectedChild = target.closest("[data-gf-fe=true][data-gf-selected=true]");

            if (closestSelectedChild.length && !closestSelectedChild.is(selectedElement)) {
                return;
            }

            if (selectedElement.is(target) || selectedElement.has(target).length > 0) {
                selectedElement.addClass("gf-fe-active-hover");
            }
        }

        builderRoot
            .on("mousedown", ".gf-fe-action-cluster [data-gf-action]", function(e) {
                e.stopPropagation();
            })
            .on("mousedown", ".gf-fe-action-menu, .gf-fe-action-menu [data-gf-action]", function(e) {
                e.stopPropagation();
            })
            .on("click", ".gf-fe-action-cluster", function(e) {
                e.stopPropagation();
            })
            .on("click", ".gf-fe-action-menu", function(e) {
                e.stopPropagation();
            })
            .on("mousemove", function(event) {
                updateActiveHoverElement(event);
            })
            .on("mouseleave", function() {
                clearActiveHoverElement();
            })
            .on("click", ".gf-fe-action-menu-trigger", function(e) {
                e.preventDefault();
                e.stopImmediatePropagation();
                e.stopPropagation();
                toggleToolbarMenu($(this));
            })
            .on("click", ".gf-fe-action-cluster [data-gf-action]", function(e) {
                if ($(this).is("[disabled]") || $(this).hasClass("disabled")) {
                    e.stopImmediatePropagation();
                    e.stopPropagation();
                    e.preventDefault();
                    return;
                }

                const action = $(this).attr("data-gf-action");
                if (action === "toggle-overflow-menu") {
                    return;
                }

                if (action === "edit") {
                    e.stopImmediatePropagation();
                    e.stopPropagation();
                    const href = $(this).attr("href") || "";
                    if (href) {
                        e.preventDefault();
                        window.location.href = href;
                    }
                    return;
                }

                if (action === "select-parent-row") {
                    e.stopImmediatePropagation();
                    e.stopPropagation();
                    e.preventDefault();
                    const row = $(this).closest(".griffinforms-app-formlayout-pagerow");
                    if (row.length && typeof selectFormElement === "function") {
                        selectFormElement(row);
                    }
                    return;
                }

                if (action === "select-parent-page") {
                    e.stopImmediatePropagation();
                    e.stopPropagation();
                    e.preventDefault();
                    const page = $(this).closest(".griffinforms-app-formlayout-formpage");
                    if (page.length && typeof selectFormElement === "function") {
                        selectFormElement(page);
                    }
                    return;
                }

                if (action === "add-product") {
                    e.stopImmediatePropagation();
                    e.stopPropagation();
                    e.preventDefault();
                    const $fw     = $(this).closest("[data-gf-itemid][data-gf-fieldtype]");
                    const $canvas = $fw.find(".gf-payment-product-canvas");
                    $canvas.find("input, textarea").val("");
                    $canvas.removeAttr("hidden");
                    $canvas.find(".gf-payment-canvas-name").focus();
                    // Scroll the presentationarea so the canvas bottom is visible
                    requestAnimationFrame(function() {
                        var $pane   = $("#griffinforms-app-presentationarea");
                        var pane    = $pane[0];
                        if (!pane) { return; }
                        var canvas  = $canvas[0];
                        var paneTop = pane.getBoundingClientRect().top;
                        var bottom  = canvas.getBoundingClientRect().bottom;
                        var visible = paneTop + pane.clientHeight;
                        if (bottom > visible) {
                            pane.scrollTop += (bottom - visible + 24);
                        }
                    });
                    return;
                }

                if (action === "toggle-empty-metadata") {
                    e.stopImmediatePropagation();
                    e.stopPropagation();
                    e.preventDefault();
                    toggleEmptyMetadataSurface($(this));
                    return;
                }
            })
            .on("click", ".gf-fe-action-menu [data-gf-action]", function(e) {
                const action = $(this).attr("data-gf-action");
                const shouldClose = ($(this).attr("data-gf-menu-close") || "true") === "true";

                e.stopImmediatePropagation();
                e.stopPropagation();

                if (action === "delete-selected-item") {
                    e.preventDefault();
                    deleteSelectedBuilderItem();
                    return;
                }

                if (action === "remove-selected-field") {
                    e.preventDefault();
                    removeSelectedReusableField();
                    return;
                }

                if (action === "change-view-cards" || action === "change-view-list") {
                    e.preventDefault();
                    const targetLayout = action === "change-view-cards" ? "cards" : "list";
                    const $fw = $(this).closest("[data-gf-itemid][data-gf-fieldtype]");
                    const fieldId = $fw.attr("data-gf-itemid");
                    if (!fieldId) { return; }
                    statusbarMsg("waiting");
                    $.post(ajaxurl, {
                        action:   "updatePaymentLayout",
                        field_id: fieldId,
                        layout:   targetLayout,
                        nonce:    gfChoiceNonce
                    }, function(response) {
                        try { response = JSON.parse(response); } catch(ex) {}
                        if (response && response.success) {
                            statusbarMsg("success", response.message + " @ " + response.time);
                            gfRefreshPaymentField($fw);
                        } else {
                            statusbarMsg("warning", response ? response.message : "Error");
                        }
                    });
                    if (shouldClose) { closeAllToolbarMenus(); }
                    return;
                }

                if (shouldClose) {
                    closeAllToolbarMenus();
                }
            });

        $(document).on("click", function(e) {
            if ($(e.target).closest(".gf-fe-action-cluster").length) {
                return;
            }
            closeAllToolbarMenus();
        });

        $(document).on("keydown", function(e) {
            if (e.key === "Escape") {
                closeAllToolbarMenus({ restoreFocus: true });
            }
        });

        document.addEventListener("item-selected", () => {
            closeAllToolbarMenus();
            if (typeof syncEmptyMetadataToggleButtons === "function") {
                syncEmptyMetadataToggleButtons(document);
            }
        });

        document.addEventListener("item-deselected", () => {
            closeAllToolbarMenus();
            if (typeof syncEmptyMetadataToggleButtons === "function") {
                syncEmptyMetadataToggleButtons(document);
            }
        });
        ';
    }

    private function moveRowBtnJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            updateRowMoveBtnState();
        });

        $("#' . esc_attr($this->getHtmlId()) . '").on("click", "[data-gf-action=move-row-up]", function(e) {
            e.preventDefault();
            const row = $(this).closest(".griffinforms-app-formlayout-pagerow");
            moveRow(row, "up");
        });

        $("#' . esc_attr($this->getHtmlId()) . '").on("click", "[data-gf-action=move-row-down]", function(e) {
            e.preventDefault();
            const row = $(this).closest(".griffinforms-app-formlayout-pagerow");
            moveRow(row, "down");
        });

        function moveRow(row, dir) {
            if (!row || !row.length) return;
            const container = row.closest(".griffinforms-app-formlayout-formpage-rows-container");
            const siblings = container.children(".griffinforms-app-formlayout-pagerow");
            const idx = siblings.index(row);
            if (idx === -1) return;
            if (dir === "up" && idx > 0) {
                row.insertBefore(siblings.eq(idx - 1));
            } else if (dir === "down" && idx < siblings.length - 1) {
                row.insertAfter(siblings.eq(idx + 1));
            } else {
                return;
            }

            const pageId = container.attr("data-gf-itemid");
            const rowIds = container.sortable("toArray", {attribute: "data-gf-itemid"});
            updateFormPageRows(pageId, rowIds);
            selectFormElement(row);
            updateRowMoveBtnState();
            // Scroll row into view
            const viewport = $("#griffinforms-app-presentationarea");
            if (viewport.length && viewport.is(":visible")) {
                const offsetTop = row.offset().top - viewport.offset().top + viewport.scrollTop();
                viewport.animate({ scrollTop: offsetTop - 60 }, 250);
            } else {
                $("html, body").animate({ scrollTop: row.offset().top - 60 }, 250);
            }
        }

        function updateRowMoveBtnState() {
            $(".griffinforms-app-formlayout-formpage-rows-container").each(function() {
                const rows = $(this).children(".griffinforms-app-formlayout-pagerow");
                rows.each(function(index) {
                    const upBtn = $(this).find(".gf-row-move-up");
                    const downBtn = $(this).find(".gf-row-move-down");
                    upBtn.prop("disabled", index === 0);
                    downBtn.prop("disabled", index === rows.length - 1);
                });
            });
        }
        ';
    }

    private function movePageBtnJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            updatePageMoveBtnState();
        });

        $("#' . esc_attr($this->getHtmlId()) . '").on("click", "[data-gf-action=move-page-up]", function(e) {
            e.preventDefault();
            const page = $(this).closest(".griffinforms-app-formlayout-formpage-container");
            selectFormElement(getInnerPageEl(page));
            movePage(page, "up");
        });

        $("#' . esc_attr($this->getHtmlId()) . '").on("click", "[data-gf-action=move-page-down]", function(e) {
            e.preventDefault();
            const page = $(this).closest(".griffinforms-app-formlayout-formpage-container");
            selectFormElement(getInnerPageEl(page));
            movePage(page, "down");
        });

        function movePage(page, dir) {
            if (!page || !page.length) return;
            const container = page.closest(".griffinforms-app-formlayout-form");
            const siblings = container.children(".griffinforms-app-formlayout-formpage-container");
            const idx = siblings.index(page);
            if (idx === -1) return;

            if (dir === "up" && idx > 0) {
                page.insertBefore(siblings.eq(idx - 1));
            } else if (dir === "down" && idx < siblings.length - 1) {
                page.insertAfter(siblings.eq(idx + 1));
            } else {
                return;
            }

            const formId = container.attr("data-gf-itemid");
            const pageIds = container.sortable("toArray", {attribute: "data-gf-itemid"});
            updateFormPageIds(formId, pageIds);
            selectFormElement(getInnerPageEl(page));
            updatePageMoveBtnState();
            refreshPageUI();
            // Scroll the moved page into view
            const target = getInnerPageEl(page);
            if (target && target.length) {
                const viewport = $("#griffinforms-app-presentationarea");
                if (viewport.length && viewport.is(":visible")) {
                    const offsetTop = target.offset().top - viewport.offset().top + viewport.scrollTop();
                    viewport.animate({ scrollTop: offsetTop - 80 }, 300);
                } else {
                    $("html, body").animate({ scrollTop: target.offset().top - 80 }, 300);
                }
            }
        }

        function getInnerPageEl(pageContainer) {
            const inner = pageContainer.find(".griffinforms-app-formlayout-formpage").first();
            return inner.length ? inner : pageContainer;
        }

        function updatePageMoveBtnState() {
            $(".griffinforms-app-formlayout-form").each(function() {
                const pages = $(this).children(".griffinforms-app-formlayout-formpage-container");
                pages.each(function(index) {
                    const upBtn = $(this).find(".gf-page-move-up");
                    const downBtn = $(this).find(".gf-page-move-down");
                    upBtn.prop("disabled", index === 0);
                    downBtn.prop("disabled", index === pages.length - 1);
                });
            });
        }
        ';
    }

    private function moveRowColumnBtnJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            updateColumnMoveBtnState();
        });

        $("#' . esc_attr($this->getHtmlId()) . '").on("click", "[data-gf-action=move-column-left]", function(e) {
            e.preventDefault();
            const col = $(this).closest(".griffinforms-app-formlayout-rowcolumn");
            moveColumn(col, "left");
        });

        $("#' . esc_attr($this->getHtmlId()) . '").on("click", "[data-gf-action=move-column-right]", function(e) {
            e.preventDefault();
            const col = $(this).closest(".griffinforms-app-formlayout-rowcolumn");
            moveColumn(col, "right");
        });

        function moveColumn(col, dir) {
            if (!col || !col.length) return;
            const container = col.closest(".griffinforms-app-formlayout-pagerow-columns-container");
            const siblings = container.children(".griffinforms-app-formlayout-rowcolumn");
            const idx = siblings.index(col);
            if (idx === -1) return;
            if (dir === "left" && idx > 0) {
                col.insertBefore(siblings.eq(idx - 1));
            } else if (dir === "right" && idx < siblings.length - 1) {
                col.insertAfter(siblings.eq(idx + 1));
            } else {
                return;
            }

            const rowId = container.attr("data-gf-itemid");
            const columnIds = container.sortable("toArray", {attribute: "data-gf-itemid"});
            updatePageRowColumns(rowId, columnIds);
            selectFormElement(col);
            updateColumnMoveBtnState();
        }

        function updateColumnMoveBtnState() {
            $(".griffinforms-app-formlayout-pagerow-columns-container").each(function() {
                const cols = $(this).children(".griffinforms-app-formlayout-rowcolumn");
                const disableDrag = cols.length <= 1;
                cols.each(function(index) {
                    const dragBtn = $(this).find(".griffinforms-app-formlayout-rowcolumn-drag-handle");
                    const leftBtn = $(this).find(".gf-col-move-left");
                    const rightBtn = $(this).find(".gf-col-move-right");
                    dragBtn.prop("disabled", disableDrag);
                    leftBtn.prop("disabled", index === 0);
                    rightBtn.prop("disabled", index === cols.length - 1);
                });
            });
        }
        ';
    }

    /**
     * Sends updated item name field values to sql for saving.
     * @see FormLayout::updateItemName()
     */
    private function updateItemNameJs()
    {
        $nonce = wp_create_nonce('update_item_name');

        echo 
        '$("#' . esc_attr($this->getHtmlId()) . '").on("mouseout blur", ".griffinforms-app-text_update_field[data-gf-fieldname=name]", function(){
            
            const field = $(this);
            let value = field.val();
            let currentValue = field.attr("data-gf-fieldvalue");
            
            if (value === "") {
                field.val(currentValue);
                return;
            }

            if (value === currentValue) {
                return;
            }

            statusbarMsg("waiting");

            const data = {
                "action": "updateItemName",
                "item_type": field.attr("data-gf-itemtype"),
                "item_id": field.attr("data-gf-itemid"),
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "field_value": value,
                "nonce": "' . esc_js($nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                
                response = JSON.parse(response);

                if (response.success) {
                    field.val(response.updated_value);
                    field.attr("data-gf-fieldvalue", response.updated_value)
                    statusbarMsg("success", response.message + " @ " + response.time);
                    if (typeof griffinformsFetchBuilderWarningCount === "function") {
                        griffinformsFetchBuilderWarningCount();
                    }
                    document.dispatchEvent(new CustomEvent("form-updated", { detail: { type: "name", itemType: data.item_type, itemId: data.item_id, formId: data.form_id, value: response.updated_value } }));
                } else {
                    statusbarMsg("warning", response.message);
                }
            });

        });';
    }

       /**
     * Sends updated item heading field values to sql for saving.
     * @see FormLayout::updateItemHeading()
     */
    private function navButtonInlineEditJs()
    {
        $nonce = wp_create_nonce('griffinforms_update_nav_label');
        echo '
        // ─── Nav button inline label editing ─────────────────────────────────────

        (function() {
            var gfNavNonce = "' . esc_js($nonce) . '";
            var gfNavTimers = {};

            $(document).on("input", "[data-gf-nav-type][contenteditable=\'true\']", function() {
                var $btn = $(this);
                var btnId = $btn.closest(\'button\').attr("id");
                clearTimeout(gfNavTimers[btnId]);
                gfNavTimers[btnId] = setTimeout(function() {
                    var value = $btn[0].textContent.trim();
                    if (value === "") {
                        $btn[0].textContent = $btn.data("gf-nav-prev-value");
                        statusbarMsg("failure", ' . wp_json_encode(__('Label cannot be empty.', 'griffinforms-form-builder')) . ');
                        return;
                    }
                    var labelType = $btn.data("gf-nav-type");
                    var pageId    = labelType !== "submit" ? ($btn.data("gf-nav-page-id") || "") : "";
                    var formId    = labelType === "submit"
                        ? ($btn.data("gf-nav-form-id") || "")
                        : ($btn.closest("[data-gf-fe=\'true\'][data-gf-itemtype=\'formpage\']").data("gf-parentid") || "");
                    statusbarMsg("waiting");
                    $.post(ajaxurl, {
                        action:      "griffinforms_update_nav_label",
                        page_id:     pageId,
                        form_id:     formId,
                        label_type:  labelType,
                        label_value: value,
                        nonce:       gfNavNonce
                    }, function(response) {
                        try { response = JSON.parse(response); } catch(e) { return; }
                        if (response.success) {
                            statusbarMsg("success", response.msg);
                            $btn.data("gf-nav-prev-value", value);
                            if (labelType === "submit") {
                                $("[data-gf-nav-type=\'submit\']").not($btn[0]).each(function() {
                                    this.textContent = value;
                                    $(this).data("gf-nav-prev-value", value);
                                });
                            }
                        } else {
                            $btn[0].textContent = $btn.data("gf-nav-prev-value");
                            statusbarMsg("failure", response.msg);
                        }
                    });
                }, 500);
            });

            $(document).on("keydown", "[data-gf-nav-type][contenteditable=\'true\']", function(e) {
                if (e.key === "Enter") { e.preventDefault(); this.blur(); }
            });

            $(document).on("blur", "[data-gf-nav-type][contenteditable=\'true\']", function() {
                if (this.textContent.trim() === "") {
                    this.textContent = $(this).data("gf-nav-prev-value");
                }
            });
        })();
        ';
    }

    private function updateItemHeadingJs()
    {
        $nonce = wp_create_nonce('update_item_heading');

        echo 
        '$("#' . esc_attr($this->getHtmlId()) . '").on("mouseout blur", ".griffinforms-app-text_update_field[data-gf-fieldname=heading]", function(){

            const field = $(this);
            const isContentEditable = this.hasAttribute("contenteditable");
            let value = isContentEditable ? this.textContent.trim() : field.val();
            let currentValue = field.attr("data-gf-fieldvalue");

            if (value === currentValue) {
                if (typeof syncBuilderMetadataSurface === "function") {
                    syncBuilderMetadataSurface(field.closest("[data-gf-structural-metadata=true]"));
                }
                return;
            }

            statusbarMsg("waiting");

            const data = {
                "action": "updateItemHeading",
                "item_type": field.attr("data-gf-itemtype"),
                "item_id": field.attr("data-gf-itemid"),
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "field_value": value,
                "nonce": "' . esc_js($nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {

                response = JSON.parse(response);

                if (response.success) {
                    if (isContentEditable) {
                        field[0].textContent = response.updated_value;
                    } else {
                        field.val(response.updated_value);
                    }
                    field.attr("data-gf-fieldvalue", response.updated_value);
                    if (typeof syncBuilderMetadataSurface === "function") {
                        syncBuilderMetadataSurface(field.closest("[data-gf-structural-metadata=true]"));
                    }
                    if (typeof syncEmptyMetadataToggleButtons === "function") {
                        syncEmptyMetadataToggleButtons(field.closest(".griffinforms-app-formlayout-formpage, .griffinforms-app-formlayout-pagerow"));
                    }
                    statusbarMsg("success", response.message + " @ " + response.time);
                    if (typeof griffinformsFetchBuilderWarningCount === "function") {
                        griffinformsFetchBuilderWarningCount();
                    }
                    document.dispatchEvent(new CustomEvent("form-updated", { detail: { type: "heading", itemType: data.item_type, itemId: data.item_id, formId: data.form_id } }));
                } else {
                    statusbarMsg("warning", response.message);
                }

            });

        });';
    }
    
    /**
     * Sends updated item description field values to sql for saving.
     * @see FormLayout::updateItemDescription()
     */
    private function updateItemDescriptionJs()
    {
        $nonce = wp_create_nonce('update_item_description');

        echo 
        '$("#' . esc_attr($this->getHtmlId()) . '").on("mouseout blur", ".griffinforms-app-textarea_update_field[data-gf-fieldname=description]", function(){
            
            const field = $(this);
            let value = field.val();
            let currentValue = field.attr("data-gf-fieldvalue");

            if (value === currentValue) {
                if (typeof syncBuilderMetadataSurface === "function") {
                    syncBuilderMetadataSurface(field.closest("[data-gf-structural-metadata=true]"));
                }
                return;
            }

            statusbarMsg("waiting");

            const data = {
                "action": "updateItemDescription",
                "item_type": field.attr("data-gf-itemtype"),
                "item_id": field.attr("data-gf-itemid"),
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "field_value": value,
                "nonce": "' . esc_js($nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                
                response = JSON.parse(response);

                if (response.success) {
                    field.val(response.updated_value);
                    field.attr("data-gf-fieldvalue", response.updated_value);
                    if (typeof syncBuilderMetadataSurface === "function") {
                        syncBuilderMetadataSurface(field.closest("[data-gf-structural-metadata=true]"));
                    }
                    if (typeof syncEmptyMetadataToggleButtons === "function") {
                        syncEmptyMetadataToggleButtons(field.closest(".griffinforms-app-formlayout-formpage, .griffinforms-app-formlayout-pagerow"));
                    }
                    statusbarMsg("success", response.message + " @ " + response.time);
                    if (typeof griffinformsFetchBuilderWarningCount === "function") {
                        griffinformsFetchBuilderWarningCount();
                    }
                    if (data.item_type === "form") {
                        document.dispatchEvent(new CustomEvent("form-updated", { detail: { type: "description", formId: data.item_id } }));
                    }
                } else {
                    statusbarMsg("warning", response.message);
                }

            });

        });';
    }

    /**
     * Sends updated step label values to sql for saving.
     * @see FormLayout::updatePageStepData()
     */
    private function updatePageStepDataJs()
    {
        $nonce = wp_create_nonce('update_page_step_data');

        echo
        '$("#' . esc_attr($this->getHtmlId()) . '").on("blur", ".griffinforms-progress-bar__label[data-gf-progress-label=true]", function(){
            
            const field = $(this);
            let value = field.val();
            let currentValue = field.attr("data-gf-fieldvalue");
            let pageId = field.attr("data-gf-itemid");

            value = value ? value.trim() : "";

            if (value === currentValue) {
                return;
            }

            statusbarMsg("waiting");

            const data = {
                "action": "updatePageStepData",
                "page_id": pageId,
                "step_name": value,
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "nonce": "' . esc_js($nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                
                response = JSON.parse(response);

                if (response.success) {
                    let updatedValue = response.updated_value && response.updated_value.step_name ? response.updated_value.step_name : "";
                    let fallback = currentValue || "";
                    let page = $(".griffinforms-app-formlayout-formpage-container[data-gf-itemid=\'" + pageId + "\']").first();
                    field.val(updatedValue !== "" ? updatedValue : fallback);
                    field.attr("data-gf-fieldvalue", updatedValue);
                    if (page.length) {
                        page.attr("data-gf-step-name", updatedValue);
                    }
                    statusbarMsg("success", response.message + " @ " + response.time);
                    if (typeof griffinformsFetchBuilderWarningCount === "function") {
                        griffinformsFetchBuilderWarningCount();
                    }
                    document.dispatchEvent(new CustomEvent("form-updated", { detail: { type: "step_data", formId: data.form_id, itemId: pageId } }));
                } else {
                    statusbarMsg("warning", response.message);
                    field.val(currentValue);
                }
            });

        });';
    }

    private function appendFormIdToEditLinksJs()
    {
        echo '
        function griffinformsAppendFormIdToEditLinks() {
            const formId = $(".griffinforms-app-formlayout-form").attr("data-gf-itemid");
            if (!formId) {
                return;
            }
            $(".gf-edit-icon-btn").each(function(){
                const link = $(this);
                const href = link.attr("href") || "";
                if (!href || href.indexOf("page=griffinforms-") === -1) {
                    return;
                }
                if (href.indexOf("form_id=") !== -1) {
                    return;
                }
                const joiner = href.indexOf("?") === -1 ? "?" : "&";
                link.attr("href", href + joiner + "form_id=" + encodeURIComponent(formId));
            });
        }
        griffinformsAppendFormIdToEditLinks();
        document.addEventListener("form-html-received", () => {
            griffinformsAppendFormIdToEditLinks();
        });
        ';
    }

    private function builderWarningCountJs()
    {
        $nonce = wp_create_nonce('form_warning_count');
        $overlay_nonce = wp_create_nonce('statusbar_overlay');
        $icon_url = $this->config->getRootUrl() . 'admin/images/statusbar-icons/warning.svg';

        echo '
        function griffinformsUpdateBuilderWarningCount(count) {
            const container = $("#griffinforms-app-statusbar .gf-statusbar-icons");
            if (!container.length) {
                return;
            }
            window.gfStatusbarCounts = window.gfStatusbarCounts || {};
            window.gfStatusbarRolling = window.gfStatusbarRolling || {};
            const config = [
                { key: "error", label: "Builder errors", icon: "error", count: 0 },
                { key: "warning", label: "Builder warnings", icon: "warning", count: 0 },
                { key: "info", label: "Builder info", icon: "info", count: 0 }
            ];
            const counts = (count && typeof count === "object") ? count : { warning: count || 0 };

            const formatCount = function(value) {
                const num = parseInt(value || 0, 10);
                if (Number.isNaN(num)) {
                    return { num: 0, text: "0" };
                }
                if (num > 99) {
                    return { num: 99, text: "99+" };
                }
                return { num: num, text: String(num) };
            };

            config.forEach(function(entry){
                const formatted = formatCount(counts[entry.key] || 0);
                const current = formatted.num;
                const currentText = formatted.text;
                let btn = container.find("[data-gf-statusbar-icon=\'" + entry.key + "\']");
                if (!btn.length) {
                    btn = $("<button type=\"button\" class=\"gf-statusbar-icon-btn\" data-gf-statusbar-icon=\"" + entry.key + "\" aria-label=\"" + entry.label + "\"></button>");
                    btn.append("<img src=\"' . esc_url($this->config->getRootUrl()) . 'admin/images/statusbar-icons/" + entry.icon + ".svg\" alt=\"\" width=\"16\" height=\"16\" />");
                    btn.append("<span class=\"gf-statusbar-icon-count\"></span>");
                    container.append(btn);
                }
                const countEl = btn.find(".gf-statusbar-icon-count");
                let prev = window.gfStatusbarCounts[entry.key];
                if (prev === undefined || Number.isNaN(parseInt(prev, 10))) {
                    prev = 0;
                    window.gfStatusbarCounts[entry.key] = 0;
                    countEl.text(0);
                }
                if (prev === current) {
                    countEl.text(currentText);
                    window.gfStatusbarRolling[entry.key] = false;
                    return;
                }
                if (window.gfStatusbarRolling[entry.key]) {
                    countEl.text(currentText);
                    window.gfStatusbarCounts[entry.key] = current;
                    window.gfStatusbarRolling[entry.key] = false;
                    return;
                }
                window.gfStatusbarRolling[entry.key] = true;
                countEl.empty();
                const roller = $("<span class=\"gf-count-roller\"></span>");
                const oldSpan = $("<span class=\"gf-count-roller-old\"></span>").text(prev > 99 ? "99+" : prev);
                const newSpan = $("<span class=\"gf-count-roller-new\"></span>").text(currentText);
                roller.append(oldSpan).append(newSpan);
                countEl.append(roller);
                requestAnimationFrame(() => {
                    roller.addClass("gf-count-roller-animate");
                });
                roller.on("animationend", function(){
                    countEl.text(currentText);
                    window.gfStatusbarCounts[entry.key] = current;
                    window.gfStatusbarRolling[entry.key] = false;
                });
            });
        }

        function griffinformsCloseStatusbarOverlay() {
            const overlay = $("#griffinforms-app-statusbar-overlay");
            overlay.attr("data-gf-open", "0").attr("aria-hidden", "true");
        }

        function griffinformsGetStatusbarPlaceholderCount(context) {
            const btn = $("#griffinforms-app-statusbar .gf-statusbar-icon-btn[data-gf-statusbar-icon=\"" + context + "\"]");
            if (!btn.length) {
                return 3;
            }

            const countText = (btn.find(".gf-statusbar-icon-count").text() || "").trim();
            const parsed = parseInt(countText.replace(/\D+/g, ""), 10);
            if (Number.isNaN(parsed) || parsed < 1) {
                return 3;
            }

            return Math.max(1, Math.min(parsed, 30));
        }

        function griffinformsRenderStatusbarOverlayPlaceholders(context) {
            const total = griffinformsGetStatusbarPlaceholderCount(context);
            let html = "<div class=\"gf-statusbar-card-list gf-statusbar-card-list-loading\">";

            for (let i = 1; i <= total; i += 1) {
                html += "<div class=\"gf-statusbar-card gf-statusbar-card-placeholder\" aria-hidden=\"true\">";
                html += "<div class=\"gf-statusbar-card-badge\">" + i + "</div>";
                html += "<div class=\"gf-statusbar-card-body\">";
                html += "<span class=\"gf-statusbar-placeholder-line gf-statusbar-placeholder-line-primary\"></span>";
                html += "<span class=\"gf-statusbar-placeholder-line gf-statusbar-placeholder-line-secondary\"></span>";
                html += "</div>";
                html += "</div>";
            }

            html += "</div>";
            return html;
        }

        function griffinformsOpenStatusbarOverlay(context) {
            const overlay = $("#griffinforms-app-statusbar-overlay");
            if (!overlay.length) {
                return;
            }
            const formId = $(".griffinforms-app-formlayout-form").attr("data-gf-itemid");
            if (!formId) {
                return;
            }
            if (overlay.attr("data-gf-open") === "1" && overlay.attr("data-gf-context") === context) {
                griffinformsCloseStatusbarOverlay();
                return;
            }
            overlay.attr("data-gf-open", "1").attr("aria-hidden", "false");
            overlay.attr("data-gf-context", context);
            overlay.find(".gf-statusbar-overlay-title").text("Loading...");
            overlay.find(".gf-statusbar-overlay-body").html(griffinformsRenderStatusbarOverlayPlaceholders(context));
            const data = {
                "action": "getStatusbarOverlayContent",
                "form_id": formId,
                "context": context,
                "nonce": "' . esc_js($overlay_nonce) . '"
            };
            $.post(ajaxurl, data, function(response) {
                let json = response;
                if (typeof response !== "object") {
                    try {
                        json = JSON.parse(response);
                    } catch (e) {
                        return;
                    }
                }
                if (json && json.success) {
                    const title = json.title || "Details";
                    const html = json.html || "";
                    overlay.find(".gf-statusbar-overlay-title").text(title);
                    overlay.find(".gf-statusbar-overlay-body").html(html);

                    const storageKey = "gf_statusbar_" + formId + "_" + context;
                    const previousCodes = sessionStorage.getItem(storageKey);
                    let previousList = [];
                    if (previousCodes) {
                        try {
                            previousList = JSON.parse(previousCodes);
                        } catch (e) {
                            previousList = [];
                        }
                    }
                    const currentCodes = [];
                    overlay.find(".gf-statusbar-card").each(function(){
                        const code = $(this).attr("data-gf-code");
                        if (code) {
                            currentCodes.push(code);
                            if (previousList.indexOf(code) === -1) {
                                $(this).addClass("gf-statusbar-card-new");
                            }
                        }
                    });
                    if (currentCodes.length) {
                        setTimeout(function(){
                            overlay.find(".gf-statusbar-card-new").removeClass("gf-statusbar-card-new");
                        }, 2400);
                        sessionStorage.setItem(storageKey, JSON.stringify(currentCodes));
                    }
                }
            });
        }

        function griffinformsFetchBuilderWarningCount() {
            const formId = $(".griffinforms-app-formlayout-form").attr("data-gf-itemid");
            if (!formId) {
                return;
            }
            const data = {
                "action": "getFormWarningCount",
                "form_id": formId,
                "nonce": "' . esc_js($nonce) . '"
            };
            $.post(ajaxurl, data, function(response) {
                let json = response;
                if (typeof response !== "object") {
                    try {
                        json = JSON.parse(response);
                    } catch (e) {
                        return;
                    }
                }
                if (json && json.success) {
                    const counts = json.counts || { warning: parseInt(json.count, 10) || 0 };
                    griffinformsUpdateBuilderWarningCount(counts);
                }
            });
        }

        document.addEventListener("form-html-received", () => {
            griffinformsFetchBuilderWarningCount();
            if (typeof syncEmptyMetadataToggleButtons === "function") {
                syncEmptyMetadataToggleButtons(document);
            }
        });

        document.addEventListener("form-updated", () => {
            griffinformsFetchBuilderWarningCount();
            if (typeof syncEmptyMetadataToggleButtons === "function") {
                syncEmptyMetadataToggleButtons(document);
            }
        });

        $(document).on("click", "#griffinforms-app-statusbar .gf-statusbar-icon-btn", function(e){
            const context = $(this).attr("data-gf-statusbar-icon");
            if (!context) {
                return;
            }
            griffinformsOpenStatusbarOverlay(context);
        });

        $(document).on("click", "#griffinforms-app-statusbar-overlay .gf-statusbar-overlay-close", function(){
            griffinformsCloseStatusbarOverlay();
        });

        $(document).on("click", function(e){
            const overlay = $("#griffinforms-app-statusbar-overlay");
            if (!overlay.length || overlay.attr("data-gf-open") !== "1") {
                return;
            }
            if ($(e.target).closest("#griffinforms-app-statusbar-overlay").length) {
                return;
            }
            if ($(e.target).closest("#griffinforms-app-statusbar .gf-statusbar-icon-btn").length) {
                return;
            }
            griffinformsCloseStatusbarOverlay();
        });

        $(document).on("keydown", function(e){
            if (e.key === "Escape") {
                griffinformsCloseStatusbarOverlay();
            }
        });

        griffinformsFetchBuilderWarningCount();
        ';
    }

    private function showAddPageJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            let pageCount = $(".griffinforms-app-formlayout-formpage").length;
            
            if (pageCount === 0) {
                $("#griffinforms-app-form-add-formpage-section").removeClass("visually-hidden");
            } else {
                $("#griffinforms-app-form-add-formpage-section").addClass("visually-hidden");
            }
        });
        ';
    }

    private function insertNewPageBtnJs()
    {
        $create_nonce = wp_create_nonce('create_formelement');
        $attach_nonce = wp_create_nonce('attach_formelement');

        echo '
        $("#' . esc_attr($this->getHtmlId()) . '").on("click", function(e){
            const actionBtn = $(e.target).closest("[data-gf-action=insert_new_page]");
            if (!actionBtn.length) {
                return;
            }
            e.preventDefault();
            createFormPage(actionBtn);
        });

        function createFormPage(btn) {
            let formId = btn.attr("data-gf-formid");
            let prevPageId = btn.attr("data-gf-prevpage");
            btn.prop("disabled", true);
            statusbarMsg("waiting");

            const data = {
                "action": "createFormElement",
                "item_type": "formpage",
                "form_id": formId,
                "nonce": "' . esc_js($create_nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);
                statusbarMsg(response.type, response.msg)

                if (response.type === "success") {
                    attachFormPage(formId, prevPageId, response.item_id);
                    btn.prop("disabled", false);
                }

            });
        }

        function attachFormPage(formId, prevPageId, pageId) {
            statusbarMsg("waiting");
            const data = {
                "action": "attachFormElement",
                "parent_type": "form",
                "item_type": "formpages",
                "parent_id": formId,
                "prev_item_id": prevPageId,
                "item_id": pageId,
                "form_id": formId,
                "nonce": "' . esc_js($attach_nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);
                statusbarMsg(response.type, response.msg);

                if (response.type === "success") {
                    if (response.html) {
                        insertPageHtmlFromResponse(formId, prevPageId, pageId, response.html);
                    } else {
                        statusbarMsg("failure", "' . esc_js(__('Unable to render page HTML.', 'griffinforms-form-builder')) . '");
                    }
                }

            });
        }

        function insertPageHtmlFromResponse(formId, prevPageId, pageId, html) {
            const inserted = $.isNumeric(prevPageId)
                ? $(html).insertAfter("#griffinforms-app-formpage-container-" + prevPageId)
                : $(html).insertAfter("#griffinforms-app-form-description");

            let newPage = inserted.first();
            if (newPage.length) {
                $("#griffinforms-app-presentationarea").animate({
                    scrollTop: $("#griffinforms-app-presentationarea").scrollTop() + newPage.position().top - 100
                }, 500);
            }

            newPage.addClass("griffinforms-new-formelement");
            selectFormElement(newPage);
            document.dispatchEvent(formHtmlReceived);
            document.dispatchEvent(new CustomEvent("form-updated", { detail: { type: "page_added", formId: formId, itemId: pageId } }));

            const rowCount = $(html).find(".griffinforms-app-formlayout-pagerow").length;
            if (rowCount === 0) {
                createStarterRow(pageId);
            }
        }

        ';
    }

    private function appendNewRowBtnJs()
    {
        $create_nonce = wp_create_nonce('create_formelement');
        $attach_nonce = wp_create_nonce('attach_formelement');

        echo '
        $("#' . esc_attr($this->getHtmlId()) . '").on("click", function(e){
            const actionBtn = $(e.target).closest("[data-gf-action=append_new_row]");
            if (!actionBtn.length) {
                return;
            }
            e.preventDefault();
            createPageRow(actionBtn);
        });

        function createStarterRow(pageId) {
            const data = {
                "action": "createFormElement",
                "item_type": "pagerow",
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "nonce": "' . esc_js($create_nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);

                if (response.type === "success") {
                    attachPageRow(pageId, response.item_id);
                }
            });
        }

        function createPageRow(btn) {
            let pageId = btn.attr("data-gf-pageid");
            let formId = $(".griffinforms-app-formlayout-form").attr("data-gf-itemid");
            btn.prop("disabled", true);
            statusbarMsg("waiting");

            const data = {
                "action": "createFormElement",
                "item_type": "pagerow",
                "form_id": formId,
                "nonce": "' . esc_js($create_nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);
                statusbarMsg(response.type, response.msg)

                if (response.type === "success") {
                    attachPageRow(pageId, response.item_id);
                    btn.prop("disabled", false);
                }

            });
        }

        function attachPageRow(pageId, rowId) {
            statusbarMsg("waiting");
            const data = {
                "action": "attachFormElement",
                "parent_type": "formpage",
                "item_type": "pagerows",
                "parent_id": pageId,
                "item_id": rowId,
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "nonce": "' . esc_js($attach_nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);
                statusbarMsg(response.type, response.msg);

                if (response.type === "success") {
                    if (response.html) {
                        insertPageRowHtmlFromResponse(pageId, rowId, response.html);
                    } else {
                        statusbarMsg("failure", "' . esc_js(__('Unable to render row HTML.', 'griffinforms-form-builder')) . '");
                    }
                }

            });
        }

        function insertPageRowHtmlFromResponse(pageId, rowId, html) {
            let rowsContainer = ".griffinforms-app-formlayout-formpage-rows-container[data-gf-itemid=" + pageId + "]";
            const inserted = $(html).appendTo($(rowsContainer));
            const newRow = inserted.filter(".griffinforms-app-formlayout-pagerow").add(inserted.find(".griffinforms-app-formlayout-pagerow")).first();
            if (!newRow.length) {
                statusbarMsg("failure", "' . esc_js(__('Unable to render row HTML.', 'griffinforms-form-builder')) . '");
                return;
            }
            newRow.addClass("griffinforms-new-formelement");
            toggleEmptyRowClass(newRow);
            selectFormElement(newRow);
            document.dispatchEvent(formHtmlReceived);
        }
        ';
    }

    private function makeColumnsDroppableJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            makeColumnsDroppable();
        });

        function makeColumnsDroppable() {
            
            $("[data-gf-itemtype=rowcolumn]").droppable({
                accept: ".gf-app-field-pill"
            });
        }';
    }

    private function highlightEmptyColumnsJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            highlightEmptyColumns();
        });
        
        function highlightEmptyColumns() {
            $(".griffinforms-app-formlayout-rowcolumn").each(function() {
            var column = $(this);
            toggleEmptyColumnClass(column);
            });
        }

        function toggleEmptyColumnClass(column) {
            if (column.find(".griffinforms-app-formlayout-field").length === 0) {
                column.addClass("griffinforms-app-formlayout-empty-rowcolumn");
            } else {
            column.removeClass("griffinforms-app-formlayout-empty-rowcolumn");
            }
        }
        ';
    }

    private function highlightEmptyRowsJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            highlightEmptyRows();
        });
        
        function highlightEmptyRows() {
            $(".griffinforms-app-formlayout-pagerow").each(function() {
                var row = $(this);
                toggleEmptyRowClass(row);
            });
        }

        function toggleEmptyRowClass(row) {
            if (row.find(".griffinforms-app-formlayout-rowcolumn").length === 0) {
                row.addClass("griffinforms-app-formlayout-empty-pagerow");
            } else {
                row.removeClass("griffinforms-app-formlayout-empty-pagerow");
            }
        }
        ';
    }

    private function insertNewFieldJs()
    {
        $create_nonce = wp_create_nonce('create_field');
        $update_nonce = wp_create_nonce('update_rowcolumn_fields');
        $get_html_nonce = wp_create_nonce('get_field_html');

        echo '
        function createField(rowColumn, placeholderField) {

            statusbarMsg("waiting");

            const data = {
                "action": "createField",
                "field_type":  placeholderField.attr("data-gf-fieldtype"),
                "nonce": "' . esc_js($create_nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);

                if (response.success) {
                    statusbarMsg("success", response.msg);
                    placeholderField.attr("data-gf-itemid", response.field_id);
                    updateRowColumnWithPlaceholder(rowColumn, placeholderField);
                } else {
                    statusbarMsg("failure", response.msg);
                    placeholderField.remove();
                }

            });
        }
        
        function updateRowColumnWithPlaceholder(rowColumn, placeholderField) {
            const fieldIds = [];
            rowColumn.children().each(function() {
                let fieldId = $(this).attr("data-gf-itemid");
                fieldIds.push(fieldId);
            });

            const data = {
                "action": "updateRowColumnFields",
                "column_id":  rowColumn.attr("data-gf-itemid"),
                "field_ids": fieldIds,
                "render_field_id": placeholderField.attr("data-gf-itemid"),
                "nonce": "' . esc_js($update_nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);

                if (response.success) {
                    statusbarMsg("success", response.msg);
                    if (response.html) {
                        const newField = $(response.html);
                        placeholderField.replaceWith(newField);
                        selectFormElement(newField);
                        document.dispatchEvent(formHtmlReceived);
                    } else {
                        replacePlaceholderField(placeholderField);
                    }
                } else {
                    statusbarMsg("failure", response.msg);
                    placeholderField.remove();
                }

            });
        }

        function replacePlaceholderField(placeholderField) {
            const data = {
                "action": "getFormElementHtml",
                "item_type": "field",
                "item_id":  placeholderField.attr("data-gf-itemid"),
                "parent_id":  0,
                "form_id": ' . esc_js(absint(wp_unslash($_GET['form_id'] ?? 0))) . ',
                "nonce": "' . esc_js($get_html_nonce) . '"
            };

            $.post(ajaxurl, data, function(response) {
                const newField = $(response);
                placeholderField.replaceWith(newField);
                selectFormElement(newField);
                document.dispatchEvent(formHtmlReceived);
            });
        }
        ';
    }

    private function makePageRowsSortableJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            makePageRowsSortable();
        });

        function makePageRowsSortable() {
            $(".griffinforms-app-formlayout-formpage-rows-container").sortable({
                items:  ".griffinforms-app-formlayout-pagerow",
                appendTo: ".griffinforms-app-formlayout-formpage-rows-container",
                connectWith: ".griffinforms-app-formlayout-formpage-rows-container",
                dropOnEmpty: true,
                cancel: "input, textarea, select, option, .griffinforms-app-text_update_field, .griffinforms-app-textarea_update_field",
                tolerance: "pointer",
                cursor: "move",
                helper: function(e, item) {
                    const clone = item.clone();
                    const targetHeight = 120;
                    const currentHeight = item.outerHeight();
                    const pos = item.position();
                    clone.css({
                        width: item.outerWidth(),
                        height: currentHeight,
                        position: "absolute",
                        top: pos.top,
                        left: pos.left,
                        overflow: "hidden",
                        margin: 0,
                        pointerEvents: "none"
                    });
                    clone.animate({ height: targetHeight }, 150);
                    return clone;
                },
                axis: "y",
                opacity: 0.9,
                distance: 5,
                forcePlaceholderSize: true,
                handle: ".griffinforms-app-formlayout-pagerow-drag-handle",
                placeholder: ".griffinforms-app-formlayout-pagerow-drag-placeholder",
                revert: 100,
                scrollSensitivity: 50,
                start: function(event, ui) {
                    ui.placeholder.css({
                        display: "block",
                        height: "120px",
                        minHeight: "120px",
                        width: ui.helper.outerWidth(),
                        margin: "0 0 6px 0",
                        padding: 0,
                        border: "1px dashed blueviolet",
                        background: "rgba(138, 43, 226, 0.15)",
                        borderRadius: "4px",
                        boxShadow: "none",
                        textAlign: "center",
                        fontSize: "13px",
                        fontWeight: "600",
                        letterSpacing: "0.4px",
                        color: "blueviolet",
                        lineHeight: "120px"
                    }).html("<span class=\'gf-placeholder-arrow\'>⬇</span> ' . esc_js(__('Drop here', 'griffinforms-form-builder')) . '");
                    ui.helper.css("transform", "");
                    if (typeof statusbarMsg === "function") {
                        statusbarMsg("waiting", "' . esc_js(__('Dragging row', 'griffinforms-form-builder')) . '");
                    }
                    ui.item.addClass("gf-row-dragging");
                    selectFormElement(ui.item);
                    $(ui.helper).addClass("shadow");
                    $(".griffinforms-app-formlayout-formpage-rows-container").addClass("rounded griffinforms-fe-droppable");
                },
                stop: function(event, ui) {
                    $(ui.item).removeClass("shadow");
                    $(".griffinforms-app-formlayout-formpage-rows-container").removeClass("rounded griffinforms-fe-droppable");
                    ui.item.removeClass("gf-row-dragging").css("display", "");
                    if (typeof statusbarMsg === "function") {
                        statusbarMsg("", "");
                    }
                },
                receive: function(event, ui) {
                    ui.item.removeClass("gf-row-dragging").css("display", "");
                    if (typeof statusbarMsg === "function") {
                        statusbarMsg("", "");
                    }
                },
                update: function(event, ui) {
                    ui.item.removeClass("gf-row-dragging").css("display", "");
                    let pageId = $(this).attr("data-gf-itemid");
                    let rowIds = $(this).sortable("toArray", {attribute: "data-gf-itemid"});
                    updateFormPageRows(pageId, rowIds);
                    $(".griffinforms-app-formlayout-formpage-rows-container").removeClass("rounded griffinforms-fe-droppable");
                    $(ui.item).removeClass("shadow");
                    if (typeof statusbarMsg === "function") {
                        statusbarMsg("", "");
                    }
                }
            });
        }';
    }

    private function updateFormPageRowsJs() {
        echo '
        function updateFormPageRows(pageId, rowIds) {

            statusbarMsg();
            const formId = $(".griffinforms-app-formlayout-form").attr("data-gf-itemid");
            
            const data = {
                "action": "updateFormPageRows",
                "page_id": pageId,
                "row_ids": rowIds,
                "form_id": formId,
                "nonce": "' . esc_js(wp_create_nonce('update_formpage_rows')) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);
                
                if (response.success) {
                    statusbarMsg("success", response.msg);
                    document.dispatchEvent(new CustomEvent("form-updated", { detail: { type: "page_order", formId: formId } }));
                } else {
                    statusbarMsg("danger", response.msg);
                }
            }).fail(function(jqXHR, textStatus) {
                statusbarMsg("danger", "Network error: " + textStatus);
            });
        }
        ';
    }

    private function makeFieldsSortableJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            makeFieldsSortable();
        });

        function makeFieldsSortable() {
            $(".griffinforms-app-formlayout-rowcolumn").sortable({
                connectWith: ".griffinforms-app-formlayout-rowcolumn",
                dropOnEmpty: true,
                cancel: "input, textarea, select, option, .griffinforms-app-text_update_field, .griffinforms-app-textarea_update_field",
                tolerance: "pointer",
                forcePlaceholderSize: true,
                cursor: "grab",
                opacity: 0.9,
                distance: 5,
                delay: 80,
                handle: ".griffinforms-app-formlayout-field-drag-handle",
                scrollSensitivity: 50,
                helper: function(e, item) {
                    const clone = item.clone();
                    const targetHeight = 80;
                    const currentHeight = item.outerHeight();
                    clone.css({
                        width: item.outerWidth(),
                        height: currentHeight,
                        overflow: "hidden",
                        margin: 0,
                        pointerEvents: "none",
                        background: "#fff",
                        opacity: 0.95
                    });
                    clone.animate({ height: targetHeight }, 150);
                    return clone;
                },
                start: function(event, ui) {
                    // ensure all column sortables are refreshed so empty targets on other pages accept drops
                    $(".griffinforms-app-formlayout-rowcolumn").sortable("refresh").sortable("refreshPositions");
                    const containerWidth = $(this).width();
                    ui.placeholder.css({
                        height: "80px",
                        minHeight: "80px",
                        padding: "0 8px",
                        border: "1px dashed blueviolet",
                        background: "rgba(138, 43, 226, 0.12)",
                        borderRadius: "4px",
                        boxShadow: "none",
                        boxSizing: "border-box",
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "center",
                        fontSize: "12px",
                        fontWeight: "600",
                        letterSpacing: "0.4px",
                        color: "blueviolet",
                        lineHeight: "normal",
                        opacity: 1,
                        visibility: "visible",
                        width: "100%"
                    }).addClass("gf-field-placeholder").html("<span class=\"gf-placeholder-arrow\" style=\"display:inline-block;margin-right:6px;\">⬇</span><span>" + "' . esc_js(__('Drop here', 'griffinforms-form-builder')) . '" + "</span>");
                    if (!ui.item.hasClass("gf-app-field-pill")) {
                        ui.item.addClass("gf-field-dragging").css("display", "none");
                    }
                    selectFormElement(ui.item);
                    $(ui.helper).addClass("shadow");
                    $(".griffinforms-app-formlayout-rowcolumn").addClass("griffinforms-fe-droppable")
                },
                stop: function(event, ui) {
                    $(ui.item).removeClass("shadow");
                    $(".griffinforms-app-formlayout-rowcolumn").removeClass("griffinforms-fe-droppable");
                    ui.item.removeClass("gf-field-dragging").css("display", "");
                    if (!ui.item.hasClass("gf-app-field-pill") && ui.helper && ui.helper.remove) {
                        ui.helper.remove();
                    }
                    $("body").css("cursor", "");
                },

                update: function(event, ui) {
                    
                    if (ui.item.hasClass("gf-app-field-pill")) {
                        return;
                    }
                        
                    let columnId = $(this).attr("data-gf-itemid");
                    let fieldIds = $(this).sortable("toArray", {attribute: "data-gf-itemid"});
                    updateRowColumnFields(columnId, fieldIds);
                    document.dispatchEvent(formHtmlReceived);
                },

                receive: function(event, ui) {
                    
                    if ($(ui.helper).hasClass("gf-app-new-field-pill")) {
                        $(ui.helper).find(".spinner-border").removeClass("visually-hidden");
                        createField($(this), ui.helper);
                    }
                    
                    if ($(ui.helper).hasClass("gf-app-reusable-field-pill")) {
                        $(ui.helper).find(".spinner-border").removeClass("visually-hidden");
                        $(ui.item).remove();
                        updateRowColumnWithPlaceholder($(this), ui.helper);
                    }

                    ui.item.removeClass("gf-field-dragging").css("display", "");
                }
            });
        }';
    }

    private function updateRowColumnFieldsJs()
    {
        echo '
        function updateRowColumnFields(rowColumnId, fieldIds) {
            const formId = $(".griffinforms-app-formlayout-form").attr("data-gf-itemid");
            
        const data = {
                "action": "updateRowColumnFields",
                "column_id":  rowColumnId,
                "field_ids": fieldIds,
                "form_id": formId,
                "nonce": "' . esc_js(wp_create_nonce('update_rowcolumn_fields')) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);

                if (response.success) {
                    statusbarMsg("success", response.msg);
                } else {
                    statusbarMsg("failure", response.msg);
                }

            }).fail(function(jqXHR, textStatus) {
                statusbarMsg("danger", "Network error: " + textStatus);
            });
        }';
    }

    private function makeRowColumnsResizableJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            makeRowColumnsResizable();
        });

        function makeRowColumnsResizable() {
            $(".griffinforms-app-formlayout-rowcolumn").resizable({
                containment: "parent",
                autoHide: false,
                handles: "e",
                start: function( event, ui ) {

                    $(this).siblings(".griffinforms-app-formlayout-pagerow-create-column-btn-container").addClass("visually-hidden");

                    const parent = $(this).parent();
                    const current = $(this);
                    const parentWidth = parent.innerWidth();
                    const currentMargins = current.outerWidth(true) - current.outerWidth();
                    const maxWidth = Math.max(0, maxAllowedWidth(parent, current) - currentMargins);
                    $(this).resizable("option", "maxWidth", maxWidth);
                    $(this).resizable("option", "minWidth", parentWidth / 4);
                },
                resize: function(event, ui) {
                    const parent = ui.element.parent();
                    const currentMargins = ui.element.outerWidth(true) - ui.element.outerWidth();
                    const available = Math.max(0, maxAllowedWidth(parent, ui.element) - currentMargins);
                    if (ui.size.width > available) {
                        ui.size.width = available;
                        ui.element.width(available);
                    }
                },
                stop: function(event, ui) {
                    updateRowColumnWidths($(this));
                    toggleCreateRowColumnBtn($(this));
                }
            });
        }

        function maxAllowedWidth(row, currentCol) {
            let usedWidth = 0;

            row.children(".griffinforms-app-formlayout-rowcolumn").each(function() {
                if (this !== currentCol[0]) {
                    usedWidth += $(this).outerWidth(true);
                }
            });

            let availableWidth = row.innerWidth() - usedWidth;

            return availableWidth;
        }
        ';
    }

    private function updateRowColumnWidthsJs()
    {
        echo '
        function updateRowColumnWidths(column) {
            let container = column.parent();
            let totalWidth = container.innerWidth();
            const columnWidths = {};
            let gridWidth = 0;

            container.children(".griffinforms-app-formlayout-rowcolumn").each(function() {
                let columnId = $(this).attr("data-gf-itemid");
                let columnWidth = Math.round(($(this).outerWidth(true) / totalWidth) * 12);
                columnWidth = Math.max(1, Math.min(12, columnWidth));
                columnWidths[columnId] = columnWidth;
                gridWidth += columnWidth;
            });

            if (gridWidth > 12) {
                const factor = 12 / gridWidth;
                gridWidth = 0;
                const ids = Object.keys(columnWidths);
                ids.forEach(function(id) {
                    columnWidths[id] = Math.max(1, Math.floor(columnWidths[id] * factor));
                    gridWidth += columnWidths[id];
                });
                const delta = 12 - gridWidth;
                const currentId = column.attr("data-gf-itemid");
                if (columnWidths[currentId]) {
                    columnWidths[currentId] = Math.max(1, Math.min(12, columnWidths[currentId] + delta));
                }
            }

            container.children(".griffinforms-app-formlayout-rowcolumn").each(function() {
                const id = $(this).attr("data-gf-itemid");
                const width = columnWidths[id] || 1;
                assignBsClass($(this), width);
            });

            const columnsData = {
                "action": "updateRowColumnWidths",
                "column_widths":  columnWidths,
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "nonce": "' . esc_js(wp_create_nonce('update_rowcolumn_widths')) . '"
            };

            $.post(ajaxurl, columnsData, function(response) {
                response = JSON.parse(response);
                
                if (response.success) {
                    statusbarMsg("success", response.msg);
                    document.dispatchEvent(formHtmlReceived);
                } else {
                    statusbarMsg("danger", response.msg);
                }
            });
        }

        function assignBsClass(column, bsWidth) {
            
            column.removeClass(function (index, className) {
                return (className.match (/(^|\s)col-\S+/g) || []).join(" ");
            });

            let bsClass = "col-md-" + bsWidth;
            column.addClass(bsClass);
            column.css("width", "");
        }
        ';
    }

    private function toggleCreateRowColumnBtnJs()
    {
        echo '
        function toggleCreateRowColumnBtn(column) {
            let container = column.parent();
            let totalWidth = container.width();
            let usedWidth = 0;

            container.children(".griffinforms-app-formlayout-rowcolumn").each(function() {
                usedWidth += $(this).width();
            });

            let aWidth = container.width() - usedWidth;
            
            if (aWidth > totalWidth / 4) {
                container.children(".griffinforms-app-formlayout-pagerow-create-column-btn-container").removeClass("visually-hidden");
            } else {
                container.children(".griffinforms-app-formlayout-pagerow-create-column-btn-container").addClass("visually-hidden");
            }

        }
        ';
    }

    private function createRowColumnBtnJs()
    {
        echo '
        $("#griffinforms-app-formlayout").on("click", ".griffinforms-app-formlayout-pagerow-create-column-btn-container", createRowColumn);

        function createRowColumn() {
            let rowId = $(this).parents(".griffinforms-app-formlayout-pagerow").attr("data-gf-itemid");
            let container = $(this).parent();
            let totalWidth = container.innerWidth();
            let columnWidth = Math.round(($(this).outerWidth() / totalWidth) * 12);
            let formId = $(".griffinforms-app-formlayout-form").attr("data-gf-itemid");

            const data = {
                "action": "createRowColumn",
                "column_width":  columnWidth,
                "form_id": formId,
                "nonce": "' . esc_js(wp_create_nonce('create_rowcolumn')) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);

                if (response.success) {
                    statusbarMsg("success", response.msg);
                    attachRowColumn(rowId, response.column_id);
                } else {
                    statusbarMsg("danger", response.msg);
                }
            }).fail(function(jqXHR, textStatus) {
                statusbarMsg("danger", "Network error: " + textStatus);
            });
        }
        
        function attachRowColumn(rowId, columnId) {
            const data = {
                "action": "attachRowColumn",
                "row_id":  rowId,
                "column_id":  columnId,
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "nonce": "' . esc_js(wp_create_nonce('attach_rowcolumn')) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);

                if (response.success) {
                    statusbarMsg("success", response.msg);
                    if (response.html) {
                        insertRowColumnHtmlFromResponse(rowId, columnId, response.html);
                    } else {
                        statusbarMsg("failure", "' . esc_js(__('Unable to render column HTML.', 'griffinforms-form-builder')) . '");
                    }
                } else {
                    statusbarMsg("failure", response.msg);
                }
            }).fail(function(jqXHR, textStatus) {
                statusbarMsg("danger", "Network error: " + textStatus);
            });
        }

        function insertRowColumnHtmlFromResponse(rowId, columnId, html) {
            let container = $(".griffinforms-app-formlayout-pagerow[data-gf-itemid=" + rowId + "]").find(".griffinforms-app-formlayout-pagerow-columns-container");
            const inserted = $(html).insertBefore(container.children(".griffinforms-app-formlayout-pagerow-create-column-btn-container"));
            const newColumn = inserted.first();
            newColumn.addClass("griffinforms-new-formelement");
            toggleCreateRowColumnBtn(newColumn);
            selectFormElement(newColumn);
            document.dispatchEvent(formHtmlReceived);
        }

        ';
    }

    private function makeRowColumnsSortableJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            makeRowColumnsSortable();
            updateColumnMoveBtnState();
        });

        function makeRowColumnsSortable() {
            $(".griffinforms-app-formlayout-pagerow-columns-container").sortable({
                cursor: "grab",
                opacity: 0.9,
                forcePlaceholderSize: true,
                axis: "x",
                handle: ".griffinforms-app-formlayout-rowcolumn-drag-handle",
                cancel: "input, textarea, select, option, .griffinforms-app-text_update_field, .griffinforms-app-textarea_update_field",
                helper: function(e, item) {
                    const clone = item.clone();
                    clone.css({
                        width: item.outerWidth(),
                        height: item.outerHeight()
                    });
                    return clone;
                },
                items: "> .griffinforms-app-formlayout-rowcolumn",
                revert: 100,
                start: function(event, ui) {
                    var parent = $(this);
                    var placeholder = ui.placeholder;
                    var helperHeight = ui.helper.outerHeight();
                    var isThemed = (typeof window.griffinformsIsThemeSet !== "undefined" && window.griffinformsIsThemeSet === 1);
                    var themeId = $(this).closest(".griffinforms-app-formlayout-form").data("gf-theme-id") || 0;
                    var helperOffsetThemes = [47,1, 14, 8, 18, 20, 27, 28, 32, 34, 35];
                    // Select the column being dragged
                    selectFormElement(ui.item);
                    if (typeof statusbarMsg === "function") {
                        statusbarMsg("waiting", "' . esc_js(__('Dragging column', 'griffinforms-form-builder')) . '");
                    }

                    // Set placeholder height to helper height to prevent collapse
                    placeholder.height(helperHeight);
                    var containerTop = $(this).offset().top;
                    var helperTop = ui.helper.offset().top;
                    var deltaY = containerTop - helperTop;
                    var translateX = (isThemed && helperOffsetThemes.includes(parseInt(themeId, 10))) ? -00 : 0;
                    ui.helper.css({
                        "z-index": 10000,
                        "pointer-events": "none",
                        "opacity": 0.95,
                        "background": "rgba(255,255,255,0.9)",
                        "transform": "translate(" + translateX + "px, " + deltaY + "px)"
                    });
                    $(ui.helper).find(".ui-resizable-handle").addClass("visually-hidden");
                    $(this).addClass("griffinforms-fe-droppable");
                },
                stop: function(event, ui) {
                    $(ui.item).find(".ui-resizable-handle").removeClass("visually-hidden")
                    $(this).removeClass("griffinforms-fe-droppable");
                    // Auto-select the dropped column
                    selectFormElement(ui.item);
                    updateColumnMoveBtnState();
                    if (typeof statusbarMsg === "function") {
                        statusbarMsg("waiting", "");
                    }
                },
                update: function(event, ui) {
                    // Reset min-height after sorting
                    $(this).css("min-height", "");
                    rowId = $(this).attr("data-gf-itemid");
                    columnIds = $(this).sortable("toArray", {attribute: "data-gf-itemid"});
                    updatePageRowColumns(rowId, columnIds);
                }
            });
        }
        ';
    }

    private function updatePageRowColumnsJs()
    {
        echo '
        function updatePageRowColumns(rowId, columnIds) {
            const data = {
                "action": "updatePageRowColumns",
                "row_id":  rowId,
                "column_ids":  columnIds,
                "form_id": $(".griffinforms-app-formlayout-form").attr("data-gf-itemid"),
                "nonce": "' . esc_js(wp_create_nonce('update_pagerow_columns')) . '"
            };

            $.post(ajaxurl, data, function(response) {
                
                response = JSON.parse(response);

                if (response.success) {
                    statusbarMsg("success", response.msg);
                } else {
                    statusbarMsg("danger", response.msg);
                }
            }).fail(function(jqXHR, textStatus) {
                statusbarMsg("danger", "Network error: " + textStatus);
            });
        }
        ';
    }

    private function makeFormPagesSortableJs()
    {
        echo '
        document.addEventListener("form-html-received", () => {
            makeFormPagesSortable();
        });

        function makeFormPagesSortable() {
            $(".griffinforms-app-formlayout-form").sortable({
                containment: "parent",
                cursor: "grab",
                axis: "y",
                opacity: 0.9,
                tolerance: "pointer",
                dropOnEmpty: true,
                cancel: "input, textarea, select, option, .griffinforms-app-text_update_field, .griffinforms-app-textarea_update_field",
                handle: ".griffinforms-app-formlayout-formpage-drag-handle",
                helper: function(e, item) {
                    const clone = item.clone();
                    const targetHeight = 120;
                    const currentHeight = item.outerHeight();
                    const pos = item.position();
                    clone.css({
                        width: item.outerWidth(),
                        height: currentHeight,
                        overflow: "hidden",
                        margin: 0,
                        pointerEvents: "none",
                        background: "var(--gf-sort-helper-bg)",
                        boxShadow: "var(--gf-sort-helper-shadow)",
                        outline: "var(--gf-sort-helper-border)",
                        borderRadius: "4px",
                        position: "absolute",
                        top: pos.top,
                        left: pos.left
                    });
                    clone.animate({ height: targetHeight }, 150);
                    return clone;
                },
                forcePlaceholderSize: true,
                items: ".griffinforms-app-formlayout-formpage-container",
                placeholder: "griffinforms-fe-droppable",
                revert: 100,
                start: function(event, ui) {
                    const container = $(this);
                    container.css({paddingBottom: "140px", paddingTop: "20px"});
                    if (!container.find(".gf-page-sort-spacer").length) {
                        const extraSpace = Math.max(320, ui.helper.outerHeight() * 2);
                        container.append("<div class=\"gf-page-sort-spacer\" style=\"height:" + extraSpace + "px; visibility:hidden; pointer-events:none;\"></div>");
                        container.css("min-height", container.outerHeight() + extraSpace);
                    }
                    if (typeof statusbarMsg === "function") {
                        statusbarMsg("waiting", "' . esc_js(__('Dragging page', 'griffinforms-form-builder')) . '");
                    }
                    ui.placeholder.css({
                        display: "block",
                        height: 120 + "px",
                        minHeight: 120 + "px",
                        width: ui.helper.outerWidth(),
                        margin: "0 0 8px 0",
                        padding: 0,
                        border: "1px dashed blueviolet",
                        background: "rgba(138, 43, 226, 0.15)",
                        borderRadius: "4px",
                        boxShadow: "none",
                        textAlign: "center",
                        fontSize: "13px",
                        fontWeight: "600",
                        letterSpacing: "0.4px",
                        color: "blueviolet",
                        lineHeight: 120 + "px"
                    }).html("<span class=\'gf-placeholder-arrow\'>⬇</span> ' . esc_js(__('Drop here', 'griffinforms-form-builder')) . '");
                },
                update: function(event, ui) {
                    $(".griffinforms-app-formlayout-form").css({paddingBottom: "", paddingTop: ""});
                    $(".gf-page-sort-spacer").remove();
                    $(".griffinforms-app-formlayout-form").css("min-height", "");
                    let pageIds = $(this).sortable("toArray", {attribute: "data-gf-itemid"});
                    let formId = $(this).attr("data-gf-itemid");
                    updateFormPageIds(formId, pageIds);
                    refreshPageUI();
                    if (typeof statusbarMsg === "function") {
                        statusbarMsg("", "");
                    }
                },
                stop: function() {
                    $(".griffinforms-app-formlayout-form").css({paddingBottom: "", paddingTop: ""});
                    $(".gf-page-sort-spacer").remove();
                    $(".griffinforms-app-formlayout-form").css("min-height", "");
                    if (typeof statusbarMsg === "function") {
                        statusbarMsg("", "");
                    }
                }
            });
        }

        function updateFormPageIds(formId, pageIds) {
            
            const data = {
                "action": "updateFormFormPages",
                "form_id":  formId,
                "page_ids":  pageIds,
                "nonce": "' . esc_js(wp_create_nonce('update_form_formpages')) . '"
            };

            $.post(ajaxurl, data, function(response) {
                response = JSON.parse(response);
                
                if (response.success) {
                    statusbarMsg("success", response.msg);
                    document.dispatchEvent(new CustomEvent("form-updated", { detail: { type: "page_order", formId: formId } }));
                } else {
                    statusbarMsg("danger", response.msg);
                }
            }).fail(function(jqXHR, textStatus) {
                statusbarMsg("danger", "Network error: " + textStatus);
            });
        }; 
        ';
    }

    public function getFormWarningCount()
    {
        $response = array(
            'success' => false,
            'count' => 0,
            'counts' => array(
                'error' => 0,
                'warning' => 0,
                'info' => 0
            )
        );

        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'form_warning_count') : false;
        if ($nonce_status === false) {
            echo wp_json_encode($response);
            wp_die();
        }

        $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;
        if ($form_id <= 0) {
            echo wp_json_encode($response);
            wp_die();
        }

        if (class_exists('\GriffinForms\Includes\Checks\Form')) {
            $checker = new \GriffinForms\Includes\Checks\Form();
            $issues = $checker->run($form_id);
            if (is_array($issues)) {
                $counts = array('error' => 0, 'warning' => 0, 'info' => 0);
                foreach ($issues as $issue) {
                    if (!is_array($issue)) {
                        continue;
                    }
                    $level = isset($issue['level']) ? (string) $issue['level'] : 'warning';
                    if (!array_key_exists($level, $counts)) {
                        $level = 'warning';
                    }
                    $counts[$level]++;
                }
                $response['counts'] = $counts;
                $response['count'] = $counts['warning'];
            }
            $response['success'] = true;
        }

        echo wp_json_encode($response);
        wp_die();
    }

    public function getStatusbarOverlayContent()
    {
        $response = array('success' => false, 'html' => '', 'title' => '');

        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'statusbar_overlay') : false;
        if ($nonce_status === false) {
            echo wp_json_encode($response);
            wp_die();
        }

        $form_id = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;
        $context = isset($_POST['context']) ? sanitize_text_field(wp_unslash($_POST['context'])) : '';

        if ($form_id <= 0 || $context === '') {
            echo wp_json_encode($response);
            wp_die();
        }

        if (in_array($context, array('error', 'warning', 'info'), true) && class_exists('\GriffinForms\Includes\Checks\Form')) {
            $checker = new \GriffinForms\Includes\Checks\Form();
            $issues = $checker->run($form_id);
            $titles = array(
                'error' => __('Errors', 'griffinforms-form-builder'),
                'warning' => __('Warnings', 'griffinforms-form-builder'),
                'info' => __('Info', 'griffinforms-form-builder'),
            );
            $response['title'] = $titles[$context] ?? __('Warnings', 'griffinforms-form-builder');

            if (!empty($issues)) {
                $allowed = \GriffinForms\Includes\Checks\Form::getAllowedHtml();
                $limit = 30;
                $context_items = [];
                foreach ($issues as $issue) {
                    if (!is_array($issue)) {
                        continue;
                    }
                    $message = $this->normalizeCheckIssueMessage($issue['message'] ?? '');
                    if ($message === '') {
                        continue;
                    }
                    $level = isset($issue['level']) ? (string) $issue['level'] : 'warning';
                    if (!in_array($level, array('error', 'warning', 'info'), true)) {
                        $level = 'warning';
                    }
                    if ($level !== $context) {
                        continue;
                    }
                    $issue['message'] = $message;
                    $context_items[] = $issue;
                }

                $total = count($context_items);
                $context_items = array_slice($context_items, 0, $limit);

                $items = [];
                $index = 1;
                foreach ($context_items as $issue) {
                    $level = isset($issue['level']) ? (string) $issue['level'] : 'warning';
                    $code = isset($issue['code']) ? sanitize_key((string) $issue['code']) : '';
                    $message = wp_kses((string) ($issue['message'] ?? ''), $allowed);
                    $code_attr = $code !== '' ? ' data-gf-code="' . esc_attr($code) . '"' : '';
                    $items[] = '<div class="gf-statusbar-card gf-statusbar-card-' . esc_attr($level) . '"' . $code_attr . '>'
                        . '<div class="gf-statusbar-card-badge">' . esc_html($index) . '</div>'
                        . '<div class="gf-statusbar-card-body">' . $message . '</div>'
                        . '</div>';
                    $index++;
                }
                if (!empty($items)) {
                    $note = '';
                    if ($total > $limit) {
                        $note = '<div class="gf-statusbar-empty">' . esc_html(sprintf(__('Showing %1$d of %2$d messages.', 'griffinforms-form-builder'), $limit, $total)) . '</div>';
                    }
                    $response['html'] = '<div class="gf-statusbar-card-list">' . implode('', $items) . '</div>' . $note;
                }
            }
            if ($response['html'] === '') {
                $empty_labels = array(
                    'error' => __('No errors found.', 'griffinforms-form-builder'),
                    'warning' => __('No warnings found.', 'griffinforms-form-builder'),
                    'info' => __('No info found.', 'griffinforms-form-builder'),
                );
                $response['html'] = '<div class="gf-statusbar-empty">' . esc_html($empty_labels[$context] ?? __('No warnings found.', 'griffinforms-form-builder')) . '</div>';
            }
            $response['success'] = true;
        }

        echo wp_json_encode($response);
        wp_die();
    }

    private function normalizeCheckIssueMessage($message): string
    {
        if (is_string($message)) {
            return $message;
        }

        if (is_scalar($message)) {
            return (string) $message;
        }

        if (is_object($message) && method_exists($message, '__toString')) {
            return (string) $message;
        }

        return '';
    }

    public function updateFieldOptions()
    {
        $response = array(
            'time' => wp_date(get_option('time_format'))
        );

        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'update_field_options') : false;

        if ($nonce_status === false) {
            $response['success'] = false;
            $response['message'] = __('Nonce error', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $field_id   = isset($_POST['field_id'])   ? absint(wp_unslash($_POST['field_id']))   : 0;
        $field_type = isset($_POST['field_type'])  ? sanitize_text_field(wp_unslash($_POST['field_type'])) : '';
        $raw_json   = isset($_POST['field_options']) ? wp_unslash($_POST['field_options'])    : '';

        if ($field_id === 0) {
            $response['success'] = false;
            $response['message'] = __('Invalid field ID', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $decoded = json_decode($raw_json, true);

        if (!is_array($decoded)) {
            $response['success'] = false;
            $response['message'] = __('Invalid options data', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $secure = \GriffinForms\Admin\Secure\Field::getInstance();
        $secure->setData(array('field_type' => $field_type));
        $secured = $secure->secureData(array('field_options' => $decoded), false);
        $secured_options = isset($secured['field_options']) ? $secured['field_options'] : array();

        $updated = $this->sql->updateItemValue('field', $field_id, 'field_options', maybe_serialize($secured_options));

        if ($updated !== false) {
            $response['success']      = true;
            $response['field_options'] = $secured_options;
            $response['message']      = __('Options updated successfully', 'griffinforms-form-builder');
        } else {
            $response['success'] = false;
            $response['message'] = __('Options could not be saved', 'griffinforms-form-builder');
        }

        echo wp_json_encode($response);
        wp_die();
    }

    public function __construct()
    {
        $this->setAll();
        add_action('wp_ajax_getFormLayoutHtml', array($this, 'getFormLayoutHtml'));
        add_action('wp_ajax_updateItemName', array($this, 'updateItemName'));
        add_action('wp_ajax_updateItemHeading', array($this, 'updateItemHeading'));
        add_action('wp_ajax_updateItemDescription', array($this, 'updateItemDescription'));
        add_action('wp_ajax_updatePageStepData', array($this, 'updatePageStepData'));
        add_action('wp_ajax_createFormElement', array($this, 'createFormElement'));
        add_action('wp_ajax_attachFormElement', array($this, 'attachFormElement'));
        add_action('wp_ajax_getFormElementHtml', array($this, 'getFormElementHtml'));
        add_action('wp_ajax_createField', array($this, 'createField'));
        add_action('wp_ajax_updateRowColumnFields', array($this, 'updateRowColumnFields'));
        add_action('wp_ajax_updateFormPageRows', array($this, 'updateFormPageRows'));
        add_action('wp_ajax_createRowColumn', array($this, 'createRowColumn'));
        add_action('wp_ajax_updateRowColumnWidths', array($this, 'updateRowColumnWidths'));
        add_action('wp_ajax_attachRowColumn', array($this, 'attachRowColumn'));
        add_action('wp_ajax_updatePageRowColumns', array($this, 'updatePageRowColumns'));
        add_action('wp_ajax_updateFormFormPages', array($this, 'updateFormFormPages'));
        add_action('wp_ajax_getFormWarningCount', array($this, 'getFormWarningCount'));
        add_action('wp_ajax_getStatusbarOverlayContent', array($this, 'getStatusbarOverlayContent'));
        add_action('wp_ajax_updateFieldOptions', array($this, 'updateFieldOptions'));
        add_action('wp_ajax_updatePaymentLayout', array($this, 'updatePaymentLayout'));
        add_action('wp_ajax_griffinforms_update_nav_label', array($this, 'updateNavLabel'));
    }

    public function updatePaymentLayout()
    {
        $response = array('time' => wp_date(get_option('time_format')));

        $nonce_status = isset($_POST['nonce']) ? wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'update_field_options') : false;
        if ($nonce_status === false) {
            $response['success'] = false;
            $response['message'] = __('Nonce error', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        $field_id = isset($_POST['field_id']) ? absint(wp_unslash($_POST['field_id'])) : 0;
        $layout   = isset($_POST['layout'])   ? sanitize_key(wp_unslash($_POST['layout'])) : '';

        if ($field_id === 0 || !in_array($layout, array('cards', 'list'), true)) {
            $response['success'] = false;
            $response['message'] = __('Invalid parameters', 'griffinforms-form-builder');
            echo wp_json_encode($response);
            wp_die();
        }

        // Load existing field_settings so we don't clobber other keys
        $raw_settings = $this->sql->getItemValue($field_id, 'field_settings', 'field');
        $existing = array();
        if (!empty($raw_settings)) {
            $decoded = json_decode($raw_settings, true);
            if (is_array($decoded)) {
                $existing = $decoded;
            }
        }
        $existing['layout'] = $layout;
        $updated = $this->sql->updateItemValue('field', $field_id, 'field_settings', wp_json_encode($existing));

        $response['success'] = ($updated !== false);
        $response['message'] = $response['success']
            ? __('Layout updated', 'griffinforms-form-builder')
            : __('Layout could not be saved', 'griffinforms-form-builder');

        echo wp_json_encode($response);
        wp_die();
    }

    public function updateNavLabel()
    {
        $this->checkNonce('griffinforms_update_nav_label');

        $page_id     = isset($_POST['page_id']) ? absint(wp_unslash($_POST['page_id'])) : 0;
        $form_id     = isset($_POST['form_id']) ? absint(wp_unslash($_POST['form_id'])) : 0;
        $label_type  = isset($_POST['label_type']) ? sanitize_text_field(wp_unslash($_POST['label_type'])) : '';
        $label_value = isset($_POST['label_value']) ? sanitize_text_field(wp_unslash($_POST['label_value'])) : '';

        if (!in_array($label_type, ['next', 'previous', 'submit'], true)) {
            $this->response['success'] = false;
            $this->response['msg'] = __('Invalid label type provided.', 'griffinforms-form-builder');
            echo wp_json_encode($this->response);
            wp_die();
        }

        if ($label_type === 'submit') {
            if (!$form_id) {
                $this->response['success'] = false;
                $this->response['msg'] = __('Form ID is missing for submit button label.', 'griffinforms-form-builder');
                echo wp_json_encode($this->response);
                wp_die();
            }
            $result = $this->sql->updateFormButtonLabel($form_id, $label_value);
        } else {
            if (!$page_id) {
                $this->response['success'] = false;
                $this->response['msg'] = __('Page ID is missing for page button label.', 'griffinforms-form-builder');
                echo wp_json_encode($this->response);
                wp_die();
            }
            $result = $this->sql->updatePageButtonLabel($page_id, $label_type, $label_value);
        }

        if ($result) {
            $this->response['success'] = true;
            $this->response['msg'] = __('Button label updated successfully.', 'griffinforms-form-builder');
        } else {
            $this->response['success'] = false;
            $this->response['msg'] = __('Failed to update button label.', 'griffinforms-form-builder');
        }

        echo wp_json_encode($this->response);
        wp_die();
    }
}
