Plugin Directory

Changeset 3473315


Ignore:
Timestamp:
03/03/2026 06:46:49 AM (9 days ago)
Author:
griffinforms
Message:

Release 2.3.4.0

Location:
griffinforms-form-builder/trunk
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • griffinforms-form-builder/trunk/admin/ajax/forms.php

    r3448124 r3473315  
    66{
    77    protected $response = [];
     8
     9    /**
     10     * Seed a new blank form with minimum starter structure:
     11     * 1 page -> 1 row -> 1 full-width (12) column.
     12     */
     13    private function seedBlankFormStarterLayout(int $form_id): bool
     14    {
     15        $form_id = absint($form_id);
     16        if ($form_id <= 0) {
     17            return false;
     18        }
     19
     20        $app_sql = \GriffinForms\Admin\Sql\App::getInstance();
     21
     22        $page_id = $app_sql->createFormPage($form_id);
     23        if (!is_int($page_id) || $page_id <= 0) {
     24            return false;
     25        }
     26
     27        $row_id = $app_sql->createPageRow($form_id);
     28        if (!is_int($row_id) || $row_id <= 0) {
     29            return false;
     30        }
     31
     32        $page_rows_updated = $app_sql->updateFormPageRows($page_id, [absint($row_id)]);
     33        if ($page_rows_updated === false) {
     34            return false;
     35        }
     36
     37        $form_pages_updated = $app_sql->updateFormFormPages($form_id, [absint($page_id)]);
     38        if ($form_pages_updated === false) {
     39            return false;
     40        }
     41
     42        // Ensure row has at least one full-width column even if auto-attach fails.
     43        $row = $app_sql->itemValues($row_id, 'pagerow');
     44        $column_ids = is_object($row) ? $this->maybeDecode($row->rowcolumns) : [];
     45        if (!is_array($column_ids) || empty($column_ids)) {
     46            $column_id = $app_sql->createRowColumn(12, $form_id);
     47            if (!is_int($column_id) || $column_id <= 0) {
     48                return false;
     49            }
     50
     51            $columns_updated = $app_sql->updatePageRowColumns($row_id, [absint($column_id)]);
     52            if ($columns_updated === false) {
     53                return false;
     54            }
     55        }
     56
     57        return true;
     58    }
    859
    960    public function exportForm() {
     
    127178   
    128179        if ($form_id) {
     180            $seeded = $this->seedBlankFormStarterLayout((int) $form_id);
     181            if (!$seeded) {
     182                // Avoid leaving behind partially initialized blank forms.
     183                $this->sql->deleteForm((int) $form_id, false);
     184                wp_send_json_error(__('Failed to initialize starter layout for blank form. Please try again.', 'griffinforms-form-builder'));
     185                wp_die();
     186            }
    129187            wp_send_json_success(['form_id' => $form_id]);
    130188        } else {
  • griffinforms-form-builder/trunk/admin/app/html/widgets/presentationarea/formlayout.php

    r3455761 r3473315  
    326326
    327327        if ($update === 1) { // When row is updated.
     328            $starter_row_id = 0;
     329
     330            // Issue 3: enforce page -> starter row server-side to avoid UI-only drift.
     331            if ($parent_type_raw === 'form' && $item_type === 'formpages') {
     332                $target_form_id = $this->resolveFormIdFromRequest([
     333                    'parent_type' => $parent_type_raw,
     334                    'parent_id' => $parent_id
     335                ]);
     336
     337                if ($target_form_id <= 0) {
     338                    $target_form_id = $parent_id;
     339                }
     340
     341                $starter_row_id = (int) $this->sql->createPageRow($target_form_id);
     342                if ($starter_row_id > 0) {
     343                    $this->sql->updateFormPageRows($item_id, array($starter_row_id));
     344                } else if (!empty($this->log)) {
     345                    $this->log->add(
     346                        'warning',
     347                        'formpage',
     348                        (int) $item_id,
     349                        get_current_user_id(),
     350                        'Unable to auto-create starter row while attaching new page.',
     351                        'builder'
     352                    );
     353                }
     354            }
     355
    328356            $response['type'] = 'success';
    329357            // translators: %s refers to the friendly name of the parent of the item added.
    330358            $response['msg'] = sprintf(__('%s was updated successfully.', 'griffinforms-form-builder'), $parent_type);
     359            if ($starter_row_id > 0) {
     360                $response['starter_row_id'] = $starter_row_id;
     361            }
    331362            $this->refreshFormLayoutMetadata(
    332363                $this->resolveFormIdFromRequest([
     
    20152046                selectFormElement(htmlId);
    20162047                document.dispatchEvent(formHtmlReceived);
    2017                 // auto-create a starter row for the new page
    2018                 createStarterRow(pageId);
     2048
     2049                // Fallback: if server-side starter-row creation did not materialize in HTML, create one client-side.
     2050                const rowCount = $(response).find(".griffinforms-app-formlayout-pagerow").length;
     2051                if (rowCount === 0) {
     2052                    createStarterRow(pageId);
     2053                }
    20192054            });
    20202055        }
  • griffinforms-form-builder/trunk/admin/app/html/widgets/rightsidebar/itemsummary.php

    r3455761 r3473315  
    66{
    77  protected $item_type;
     8  protected $delete_guard_message = '';
    89 
    910  public function getHtml()
     
    4041        $this->loadItem();
    4142      }
     43
     44      $this->delete_guard_message = $this->getMinimumStructureDeleteGuardMessage($this->item_type, $this->item_id);
    4245
    4346      $this->itemSummary();
     
    140143      }
    141144
     145      $data_attrs = [
     146          'data-gf-itemtype=' . $this->item_type,
     147          'data-gf-itemid=' . $this->item_id
     148      ];
     149
     150      if ($this->delete_guard_message !== '') {
     151          $data_attrs[] = 'data-gf-guardmsg=' . rawurlencode($this->delete_guard_message);
     152          $data_attrs[] = 'data-gf-disabled=1';
     153      }
     154
    142155      $this->buttons->bsBtn([
    143           'class' => 'gf-btn-light btn-sm flex-fill me-1',
     156          'class' => 'gf-btn-light btn-sm flex-fill me-1' . ($this->delete_guard_message !== '' ? ' disabled' : ''),
    144157          'id'    => $this->getHtmlId('delete-item-btn'),
    145158          // Translators: %s refers to the friendly name of the item type (e.g., Field, Page).
    146159          'label' => sprintf(__('Delete %s', 'griffinforms-form-builder'), $this->config->getFriendlyName($this->item_type)),
    147160          'spinner' => true,
    148           'data'  => [
    149               'data-gf-itemtype=' . $this->item_type,
    150               'data-gf-itemid=' . $this->item_id
    151           ]
     161          'aria' => $this->delete_guard_message !== '' ? ['aria-disabled' => 'true'] : [],
     162          'data'  => $data_attrs
    152163      ]);
    153164  }
     
    268279   
    269280    $(wrapperId).on("click", btnId, function(){
     281      const guardMsgEncoded = $(this).attr("data-gf-guardmsg") || "";
     282      if (guardMsgEncoded !== "") {
     283        const guardMsg = decodeURIComponent(guardMsgEncoded);
     284        alert(guardMsg);
     285        statusbarMsg("failure", guardMsg);
     286        return;
     287      }
    270288   
    271289      showBtnSpinner(btnId);
     
    369387          $this->response['success'] = false;
    370388          $this->response['msg'] = sprintf(__('Unable to delete %s.', 'griffinforms-form-builder'), $this->config->getFriendlyName($item_type));
     389          echo wp_json_encode($this->response);
     390          wp_die();
     391      }
     392
     393      $guard_message = $this->getMinimumStructureDeleteGuardMessage($item_type, $item_id);
     394      if ($guard_message !== '') {
     395          $this->response['success'] = false;
     396          $this->response['msg'] = $guard_message;
    371397          echo wp_json_encode($this->response);
    372398          wp_die();
     
    586612  }
    587613
     614  private function findPageIdByRowId(int $row_id): int
     615  {
     616      if ($row_id <= 0) {
     617          return 0;
     618      }
     619
     620      global $wpdb;
     621      $page_table = $this->config->getTable('formpage');
     622      $like_str = '%' . $wpdb->esc_like((string) $row_id) . '%';
     623      $page_id = $wpdb->get_var(
     624          $wpdb->prepare(
     625              "SELECT id FROM %i WHERE pagerows LIKE %s LIMIT 1",
     626              $page_table,
     627              $like_str
     628          )
     629      );
     630
     631      return $page_id ? (int) $page_id : 0;
     632  }
     633
     634  private function findRowIdByColumnId(int $column_id): int
     635  {
     636      if ($column_id <= 0) {
     637          return 0;
     638      }
     639
     640      global $wpdb;
     641      $row_table = $this->config->getTable('pagerow');
     642      $like_str = '%' . $wpdb->esc_like((string) $column_id) . '%';
     643      $row_id = $wpdb->get_var(
     644          $wpdb->prepare(
     645              "SELECT id FROM %i WHERE rowcolumns LIKE %s LIMIT 1",
     646              $row_table,
     647              $like_str
     648          )
     649      );
     650
     651      return $row_id ? (int) $row_id : 0;
     652  }
     653
    588654  private function findFormIdByColumnId(int $column_id): int
    589655  {
     
    618684
    619685      return $row_id ? $this->findFormIdByRowId((int) $row_id) : 0;
     686  }
     687
     688  private function getMinimumStructureDeleteGuardMessage(string $item_type, int $item_id): string
     689  {
     690      $item_type = strtolower(trim($item_type));
     691      $item_id = absint($item_id);
     692
     693      if ($item_id <= 0) {
     694          return '';
     695      }
     696
     697      if ($item_type === 'formpage') {
     698          $form_id = $this->findFormIdByPageId($item_id);
     699          if ($form_id <= 0 || !$this->sql->itemExists('form', $form_id)) {
     700              return '';
     701          }
     702          $form = $this->sql->itemValues($form_id, 'form');
     703          $page_ids = is_object($form) ? $this->maybeDecode($form->formpages) : [];
     704          $count = is_array($page_ids) ? count($page_ids) : 0;
     705          if ($count <= 1) {
     706              return __('You must keep at least one page in the form.', 'griffinforms-form-builder');
     707          }
     708      }
     709
     710      if ($item_type === 'pagerow') {
     711          $page_id = $this->findPageIdByRowId($item_id);
     712          if ($page_id <= 0 || !$this->sql->itemExists('formpage', $page_id)) {
     713              return '';
     714          }
     715          $page = $this->sql->itemValues($page_id, 'formpage');
     716          $row_ids = is_object($page) ? $this->maybeDecode($page->pagerows) : [];
     717          $count = is_array($row_ids) ? count($row_ids) : 0;
     718          if ($count <= 1) {
     719              return __('You must keep at least one row in the page.', 'griffinforms-form-builder');
     720          }
     721      }
     722
     723      if ($item_type === 'rowcolumn') {
     724          $row_id = $this->findRowIdByColumnId($item_id);
     725          if ($row_id <= 0 || !$this->sql->itemExists('pagerow', $row_id)) {
     726              return '';
     727          }
     728          $row = $this->sql->itemValues($row_id, 'pagerow');
     729          $column_ids = is_object($row) ? $this->maybeDecode($row->rowcolumns) : [];
     730          $count = is_array($column_ids) ? count($column_ids) : 0;
     731          if ($count <= 1) {
     732              return __('You must keep at least one column in the row.', 'griffinforms-form-builder');
     733          }
     734      }
     735
     736      return '';
    620737  }
    621738
  • griffinforms-form-builder/trunk/admin/css/griffinforms-forms.css

    r3394319 r3473315  
    2727
    2828.griffinforms-templates-category-tab-pane {
    29     overflow: hidden;
    30     overflow-y: scroll;
     29    overflow: visible;
     30    max-height: none;
     31}
     32
     33#griffinforms-create-form-modal-content {
     34    height: 650px;
    3135}
    3236
    3337#griffinforms-create-form-modal-content .tab-pane {
    34     min-height: 500px;
    35     overflow: hidden;
     38    min-height: 0;
     39    overflow: hidden;
     40}
     41
     42.gf-create-form-template-layout {
     43    align-items: flex-start;
     44}
     45
     46#griffinforms-create-form-modal-category-list {
     47    flex: 0 0 170px;
     48    max-height: 500px;
     49    display: flex;
     50    flex-direction: column !important;
     51    flex-wrap: nowrap !important;
     52    overflow-y: auto;
     53    overflow-x: hidden;
     54    scrollbar-gutter: stable both-edges;
     55    padding-right: 12px;
     56    align-self: flex-start;
     57}
     58
     59#griffinforms-create-form-modal-category-list .list-group-item {
     60    width: 100%;
     61    flex: 0 0 auto;
     62    margin-right: 2px;
    3663}
    3764
    3865#griffinforms-create-form-modal-templates-search-input {
    3966    border-color: #dee2e6 !important;
     67}
     68
     69.gf-create-form-template-search-row {
     70    padding-right: 1rem;
    4071}
    4172
     
    4677
    4778#griffinforms-form-templates-container {
    48     overflow-y: scroll;
     79    height: 500px;
     80    overflow-y: auto;
     81    overflow-x: hidden;
     82}
     83
     84@media (max-width: 991.98px) {
     85    #griffinforms-create-form-modal-content {
     86        height: 560px;
     87    }
     88
     89    #griffinforms-create-form-modal-category-list,
     90    #griffinforms-form-templates-container {
     91        height: 420px;
     92        max-height: 420px;
     93    }
     94
    4995}
    5096
     
    79125    position: relative;
    80126    transition: transform 0.3s ease; /* Smooth transition for initial loading */
     127}
     128
     129.griffinforms-blank-template-card .griffinforms-template-preview {
     130    background: linear-gradient(135deg, #eef4ff 0%, #f7fbff 100%);
     131}
     132
     133.gf-blank-template-icon {
     134    font-size: 24px;
     135    width: 24px;
     136    height: 24px;
     137    color: #2271b1;
     138}
     139
     140.gf-template-skeleton-card {
     141    border: 1px solid #e9edf2;
     142    background: #fff;
     143}
     144
     145.gf-template-skeleton-preview {
     146    height: 120px;
     147    border-bottom: 1px solid #eef2f6;
     148    background: linear-gradient(110deg, #f2f4f7 8%, #e7ebf0 18%, #f2f4f7 33%);
     149    background-size: 200% 100%;
     150    animation: gf-skeleton-shimmer 1.2s linear infinite;
     151}
     152
     153.gf-template-skeleton-line {
     154    height: 10px;
     155    margin-bottom: 8px;
     156    border-radius: 4px;
     157    background: linear-gradient(110deg, #f2f4f7 8%, #e7ebf0 18%, #f2f4f7 33%);
     158    background-size: 200% 100%;
     159    animation: gf-skeleton-shimmer 1.2s linear infinite;
     160}
     161
     162.gf-template-skeleton-line-title {
     163    width: 70%;
     164    height: 12px;
     165}
     166
     167.gf-template-skeleton-line-body {
     168    width: 92%;
     169}
     170
     171.gf-template-skeleton-line-body.short {
     172    width: 58%;
     173}
     174
     175.gf-template-skeleton-button {
     176    width: 48%;
     177    height: 26px;
     178    margin-top: 10px;
     179    border-radius: 4px;
     180    background: linear-gradient(110deg, #f2f4f7 8%, #e7ebf0 18%, #f2f4f7 33%);
     181    background-size: 200% 100%;
     182    animation: gf-skeleton-shimmer 1.2s linear infinite;
     183}
     184
     185@keyframes gf-skeleton-shimmer {
     186    to {
     187        background-position-x: -200%;
     188    }
    81189}
    82190
     
    103211.griffinforms-template-thumbnail .row {
    104212    font-size: 0.9em;
     213}
     214
     215/* Keep template card CTA rows aligned even when descriptions have fewer lines. */
     216.griffinforms-form-template-card .card-text {
     217    line-height: 1.4;
     218    min-height: calc(1.4em * 3);
     219    display: -webkit-box;
     220    -webkit-box-orient: vertical;
     221    -webkit-line-clamp: 3;
     222    overflow: hidden;
     223    text-overflow: ellipsis;
     224}
     225
     226.griffinforms-form-template-card .card-text[title] {
     227    cursor: help;
    105228}
    106229
     
    226349    margin-bottom: -2px;
    227350    color: #000 !important;
     351    position: relative;
     352    border: 0 !important;
     353    background: transparent !important;
     354}
     355
     356#griffinforms-create-form-modal-tabs {
     357    position: relative;
     358}
     359
     360#griffinforms-create-form-modal-tabs .gf-create-form-nav-accent {
     361    position: absolute;
     362    bottom: -2px;
     363    height: 2px;
     364    width: 0;
     365    background: #2271b1;
     366    transform: translateX(0);
     367    transition: transform 280ms ease, width 280ms ease, opacity 120ms ease;
     368    opacity: 0;
     369    pointer-events: none;
    228370}
    229371
    230372#griffinforms-create-form-modal-tabs .nav-link.active {
    231     border-color: #2271b1;
    232373    color: #2271b1 !important;
    233374}
  • griffinforms-form-builder/trunk/admin/html/elements/templatecard.php

    r3377809 r3473315  
    8888        $html .= $badge;
    8989        $html .= '<div class="card-body p-2">';
     90        $description = $template['description'] ?? __('No description available.', 'griffinforms-form-builder');
    9091        $html .= '<div class="card-title small fw-bold">' . esc_html($template['name'] ?? __('Untitled', 'griffinforms-form-builder')) . '</div>';
    91         $html .= '<p class="card-text small text-muted">' . esc_html($template['description'] ?? __('No description available.', 'griffinforms-form-builder')) . '</p>';
     92        $html .= '<p class="card-text small text-muted" title="' . esc_attr($description) . '">' . esc_html($description) . '</p>';
    9293       
    9394        $html .= '<div class="text-center">';
  • griffinforms-form-builder/trunk/admin/html/modals/createform.php

    r3377809 r3473315  
    5757        echo '<button class="nav-link" id="griffinforms-create-form-modal-upload-template-tab" data-bs-toggle="tab" data-bs-target="#griffinforms-create-form-modal-upload-template" type="button" role="tab">' . esc_html(__('Import as Template', 'griffinforms-form-builder')) . '</button>';
    5858        echo '</div></nav>';
    59         echo '<div class="tab-content p-3" id="griffinforms-create-form-modal-content" style="height: 650px;">';
     59        echo '<div class="tab-content p-3" id="griffinforms-create-form-modal-content">';
    6060
    6161        $this->renderTabsContent();
     
    9595    protected function templateSearch()
    9696    {
    97         echo '<div class="d-flex align-items-center justify-content-end position-relative">';
     97        echo '<div class="d-flex align-items-center justify-content-end position-relative gf-create-form-template-search-row">';
    9898
    9999        echo '<span id="griffinforms-create-form-modal-template-search-spinner" class="spinner position-relative me-2" style="visibility: visible; bottom: 8px;"></span>';
     
    131131        $content_html = '';
    132132
    133         echo '<div class="d-flex align-items-start">';
     133        echo '<div class="gf-create-form-template-layout d-flex align-items-start">';
    134134        echo '<div class="nav flex-column list-group small me-3" id="griffinforms-create-form-modal-category-list" role="tablist">';
    135135
     
    151151
    152152            $content_html .= '<div class="griffinforms-templates-category-tab-pane tab-pane fade pe-3' . esc_attr($active_content) . '" id="' . esc_attr($content_id) . '" role="tabpanel" tabindex="0">';
    153             $content_html .= '<div class="row row-cols-1 row-cols-md-4 g-3"></div>';
     153            $content_html .= '<div class="row row-cols-1 row-cols-md-4 g-3">';
     154            if ($category_slug === 'all') {
     155                $content_html .= $this->blankTemplateQuickCardHtml();
     156            }
     157            $content_html .= '</div>';
    154158            $content_html .= '<div class="griffinforms-templates-category-info text-center mt-3 small text-muted p-3" style="display: none;"></div>';
    155159            $content_html .='</div>';
     
    158162        echo '</div>';
    159163
    160         echo '<div class="tab-content w-100" id="griffinforms-form-templates-container" style="height: 500px;">';
     164        echo '<div class="tab-content w-100" id="griffinforms-form-templates-container">';
    161165        echo wp_kses_post($content_html);
    162166        echo '</div>';
    163167        echo '</div>';
     168    }
     169
     170    protected function blankTemplateQuickCardHtml()
     171    {
     172        $html  = '<div class="col">';
     173        $html .= '<div class="griffinforms-form-template-card griffinforms-blank-template-card card small p-0 mt-0 position-relative overflow-hidden shadow-none" style="width: auto; height: 100%">';
     174        $html .= '<div class="griffinforms-template-preview mb-1 bg-light d-flex align-items-center justify-content-center">';
     175        $html .= '<span class="dashicons dashicons-plus-alt2 gf-blank-template-icon" aria-hidden="true"></span>';
     176        $html .= '</div>';
     177        $html .= '<div class="card-body p-2">';
     178        $html .= '<div class="card-title small fw-bold">' . esc_html__('Create Blank Form', 'griffinforms-form-builder') . '</div>';
     179        $blank_desc = __('Start from scratch with default starter settings.', 'griffinforms-form-builder');
     180        $html .= '<p class="card-text small text-muted" title="' . esc_attr($blank_desc) . '">' . esc_html($blank_desc) . '</p>';
     181        $html .= '<div class="text-center">';
     182        $html .= '<button type="button" class="griffinforms-create-blank-form-quick button button-secondary button-small text-center">';
     183        $html .= '<span>' . esc_html__('Create Form', 'griffinforms-form-builder') . '</span>';
     184        $html .= '</button>';
     185        $html .= '</div>';
     186        $html .= '</div>';
     187        $html .= '</div>';
     188        $html .= '</div>';
     189
     190        return $html;
    164191    }
    165192
     
    275302    {
    276303        $js  = 'jQuery(document).ready(function($){' . PHP_EOL;
     304        $js .= 'const blankFormDefaultName = "' . esc_js(__('Blank Form', 'griffinforms-form-builder')) . '";' . PHP_EOL;
     305        $js .= 'const blankFormCardHtml = ' . wp_json_encode($this->blankTemplateQuickCardHtml()) . ';' . PHP_EOL;
    277306        $js .= 'const getTemplatesNonce = "' . esc_js(wp_create_nonce('get_form_templates')) . '";' . PHP_EOL;
    278307        $js .= 'const formFromTemplateNonce = "' . esc_js(wp_create_nonce('form_from_template')) . '";' . PHP_EOL;
     
    282311        $js .= 'const ajaxFailMsg = "' . esc_js(__('Network request failed. Try again later.', 'griffinforms-form-builder')) . '";' . PHP_EOL;
    283312        $js .= $this->showErrorMessageJs() . PHP_EOL;
     313        $js .= $this->slidingNavAccentJs() . PHP_EOL;
    284314        $js .= $this->hideErrorsOnTabSwitchJs() . PHP_EOL;
    285315        $js .= $this->fetchTemplatesJs() . PHP_EOL;
     
    314344    }
    315345
     346    protected function slidingNavAccentJs()
     347    {
     348        return '
     349        const tabsNav = document.getElementById("griffinforms-create-form-modal-tabs");
     350        const createFormModalEl = document.getElementById("' . esc_attr($this->id) . '");
     351        let navAccent = null;
     352
     353        function ensureNavAccent() {
     354            if (!tabsNav) {
     355                return null;
     356            }
     357            if (!navAccent) {
     358                navAccent = tabsNav.querySelector(".gf-create-form-nav-accent");
     359            }
     360            if (!navAccent) {
     361                navAccent = document.createElement("span");
     362                navAccent.className = "gf-create-form-nav-accent";
     363                tabsNav.appendChild(navAccent);
     364            }
     365            return navAccent;
     366        }
     367
     368        function moveNavAccentTo(button) {
     369            const accent = ensureNavAccent();
     370            if (!accent || !button || !tabsNav.contains(button)) {
     371                return false;
     372            }
     373
     374            const navRect = tabsNav.getBoundingClientRect();
     375            const btnRect = button.getBoundingClientRect();
     376            if (navRect.width < 20 || btnRect.width < 20 || btnRect.height < 20) {
     377                accent.style.opacity = "0";
     378                return false;
     379            }
     380            const inset = Math.max(8, Math.floor(btnRect.width * 0.12));
     381            const width = Math.max(24, Math.floor(btnRect.width - (inset * 2)));
     382            const x = Math.floor(btnRect.left - navRect.left + inset);
     383
     384            accent.style.width = width + "px";
     385            accent.style.transform = "translateX(" + x + "px)";
     386            accent.style.opacity = "1";
     387            return true;
     388        }
     389
     390        function moveNavAccentToActive() {
     391            const activeBtn = tabsNav ? tabsNav.querySelector(".nav-link.active") : null;
     392            if (activeBtn) {
     393                const moved = moveNavAccentTo(activeBtn);
     394                if (!moved) {
     395                    setTimeout(function () {
     396                        moveNavAccentTo(activeBtn);
     397                    }, 80);
     398                }
     399            }
     400        }
     401
     402        if (tabsNav) {
     403            ensureNavAccent();
     404            moveNavAccentToActive();
     405
     406            tabsNav.querySelectorAll(".nav-link").forEach((btn) => {
     407                btn.addEventListener("mouseenter", function() {
     408                    moveNavAccentTo(btn);
     409                });
     410                btn.addEventListener("focus", function() {
     411                    moveNavAccentTo(btn);
     412                });
     413            });
     414
     415            tabsNav.addEventListener("mouseleave", function() {
     416                moveNavAccentToActive();
     417            });
     418
     419            $(document).on("shown.bs.tab", "#griffinforms-create-form-modal-tabs button", function (e) {
     420                if (e && e.target) {
     421                    moveNavAccentTo(e.target);
     422                } else {
     423                    moveNavAccentToActive();
     424                }
     425            });
     426
     427            window.addEventListener("resize", function () {
     428                moveNavAccentToActive();
     429            });
     430
     431            if (createFormModalEl) {
     432                createFormModalEl.addEventListener("shown.bs.modal", function () {
     433                    moveNavAccentToActive();
     434                    setTimeout(moveNavAccentToActive, 80);
     435                });
     436            }
     437        }
     438        ';
     439    }
     440
    316441    protected function hideErrorsOnTabSwitchJs()
    317442    {
     
    332457        const ended = {};
    333458        const loading = {};
     459
     460        function skeletonCardsHtml(count = 8) {
     461            let cards = "";
     462            for (let i = 0; i < count; i++) {
     463                cards += "<div class=\"col\">" +
     464                    "<div class=\"gf-template-skeleton-card card small p-0 mt-0 position-relative overflow-hidden shadow-none\" style=\"width: auto; height: 100%\">" +
     465                        "<div class=\"gf-template-skeleton-preview\"></div>" +
     466                        "<div class=\"card-body p-2\">" +
     467                            "<div class=\"gf-template-skeleton-line gf-template-skeleton-line-title\"></div>" +
     468                            "<div class=\"gf-template-skeleton-line gf-template-skeleton-line-body\"></div>" +
     469                            "<div class=\"gf-template-skeleton-line gf-template-skeleton-line-body short\"></div>" +
     470                            "<div class=\"gf-template-skeleton-button\"></div>" +
     471                        "</div>" +
     472                    "</div>" +
     473                "</div>";
     474            }
     475            return cards;
     476        }
    334477
    335478        function fetchTemplates(categorySlug = "all", categoryLabel = "All", isInitialLoad = false) {
     
    346489                const gridRow = pane.children(".row").first();
    347490                gridRow.html(""); // Clear existing templates in target pane
     491                if (categorySlug === "all") {
     492                    gridRow.append(blankFormCardHtml);
     493                }
     494                gridRow.append(skeletonCardsHtml());
    348495            }
    349496
     
    366513                },
    367514                success: function(response) {
     515                    const pane = $("#griffinforms-create-form-modal-category-" + categorySlug + "-content");
     516                    const gridRow = pane.children(".row").first();
     517                    if (isInitialLoad) {
     518                        gridRow.html("");
     519                        if (categorySlug === "all") {
     520                            gridRow.append(blankFormCardHtml);
     521                        }
     522                    }
    368523                    if (response.success) {
    369                         const pane = $("#griffinforms-create-form-modal-category-" + categorySlug + "-content");
    370                         const gridRow = pane.children(".row").first();
    371524                        gridRow.append(response.data); // Append new templates with server-rendered previews
    372525                        // Increment offset for the next fetch
     
    376529                            showErrorMessage(response.data && response.data.message ? response.data.message : (response.data || ""));
    377530                        } else {
    378                             const pane = $("#griffinforms-create-form-modal-category-" + categorySlug + "-content");
    379531                            pane.find(".griffinforms-templates-category-info").text(response.data.message).show();
    380532                            ended[categorySlug] = true;
     
    389541        }
    390542
    391         // Event Listener for Scroll to Load More (only trigger on downward scroll)
    392         let lastScrollTop = 0;
    393         $("#griffinforms-form-templates-container").on("scroll", function () {
    394             const currentScroll = $(this).scrollTop();
    395             const isScrollingDown = currentScroll > lastScrollTop;
    396             lastScrollTop = currentScroll;
    397 
    398             // Only fetch when scrolling down and near bottom
    399             if (isScrollingDown && currentScroll + $(this).innerHeight() >= this.scrollHeight - 100) {
     543        function maybeFetchMore($scroller) {
     544            const currentScroll = $scroller.scrollTop();
     545            const innerHeight = $scroller.innerHeight();
     546            const scrollHeight = $scroller.get(0) ? $scroller.get(0).scrollHeight : 0;
     547
     548            // Fetch when near bottom; loading/ended guards prevent duplicate fetches.
     549            if (currentScroll + innerHeight >= scrollHeight - 120) {
    400550                const activeTab = $(".griffinforms-templates-category-tab.active");
    401551                const categorySlug = activeTab.attr("data-gf-category") || "all";
     
    404554                fetchTemplates(categorySlug, categoryLabel);
    405555            }
     556        }
     557
     558        // Main infinite-load listener (templates container is the scroll owner).
     559        $("#griffinforms-form-templates-container").on("scroll", function () {
     560            maybeFetchMore($(this));
    406561        });
    407562        ';
     
    441596    protected function createBlankFormJs() {
    442597        $js = '
    443         $("#griffinforms-create-form-modal-blank-form-btn").click(function() {
    444             let formName = $("#griffinforms-create-form-modal-blank-form-name").val();
    445             let formDescription = $("#griffinforms-create-form-modal-blank-form-description").val();
    446            
     598        let blankCreateInFlight = false;
     599
     600        function setBlankCreateButtonsDisabled(isDisabled) {
     601            const blankBtn = $("#griffinforms-create-form-modal-blank-form-btn");
     602            const quickBtn = $("#griffinforms-form-templates-container .griffinforms-create-blank-form-quick");
     603            if (isDisabled) {
     604                blankBtn.attr("disabled", true);
     605                quickBtn.attr("disabled", true);
     606            } else {
     607                blankBtn.removeAttr("disabled");
     608                quickBtn.removeAttr("disabled");
     609            }
     610        }
     611
     612        function submitBlankForm(formName, formDescription) {
     613            if (blankCreateInFlight) {
     614                return;
     615            }
     616
    447617            clearErrorMessage();
    448    
    449             if (!formName.trim()) {
    450                 showErrorMessage("' . esc_js(__('Form name cannot be empty.', 'griffinforms-form-builder')) . '");
    451                 return;
    452             }
    453    
     618
     619            if (!formName || !formName.trim()) {
     620                formName = blankFormDefaultName;
     621            }
     622
     623            blankCreateInFlight = true;
     624            setBlankCreateButtonsDisabled(true);
     625
    454626            const data = {
    455627                action: "createBlankForm",
     
    466638                } else {
    467639                    showErrorMessage(response.data);
     640                    blankCreateInFlight = false;
     641                    setBlankCreateButtonsDisabled(false);
    468642                }
    469643            }).fail(function() {
    470644                showErrorMessage("' . esc_js(__('Network request failed. Try again later.', 'griffinforms-form-builder')) . '");
     645                blankCreateInFlight = false;
     646                setBlankCreateButtonsDisabled(false);
    471647            });
     648        }
     649
     650        $("#griffinforms-create-form-modal-blank-form-btn").click(function() {
     651            let formName = $("#griffinforms-create-form-modal-blank-form-name").val();
     652            let formDescription = $("#griffinforms-create-form-modal-blank-form-description").val();
     653
     654            if (!formName || !formName.trim()) {
     655                formName = blankFormDefaultName;
     656                $("#griffinforms-create-form-modal-blank-form-name").val(formName);
     657            }
     658
     659            submitBlankForm(formName, formDescription);
    472660        });
     661
     662        $("#griffinforms-form-templates-container").on("click", ".griffinforms-create-blank-form-quick", function() {
     663            submitBlankForm(blankFormDefaultName, "");
     664        });
    473665        ';
    474666        return $js;
     
    478670    {
    479671        $js = '
     672        let templateCreateInFlight = false;
     673
    480674        $("#griffinforms-form-templates-container").on("click", ".griffinforms-create-form-from-template", function() {
     675            if (templateCreateInFlight) {
     676                return;
     677            }
     678            templateCreateInFlight = true;
    481679            clearErrorMessage();
    482680            let templateId = $(this).attr("data-gf-templateid");
     
    509707                // Hide spinner
    510708                $("#griffinforms-create-form-modal-template-search-spinner").css("visibility", "hidden");
     709                templateCreateInFlight = false;
    511710            });
    512711        });
     
    663862    {
    664863        return '
     864        const minSearchChars = 3;
     865        const minSearchMsg = "' . esc_js(__('Type at least 3 characters to search templates.', 'griffinforms-form-builder')) . '";
     866
     867        function showSearchHint(message) {
     868            const searchTab = $("#griffinforms-create-form-modal-category-search-tab");
     869            searchTab.removeClass("d-none");
     870            searchTab.tab("show");
     871            const searchPane = $("#griffinforms-create-form-modal-category-search-content");
     872            const gridRow = searchPane.children(".row").first();
     873            gridRow.html("");
     874            searchPane.find(".griffinforms-templates-category-info").text(message).show();
     875            $("#griffinforms-create-form-modal-category-search-tab .badge").html(0);
     876        }
     877
     878        function clearSearchHint() {
     879            const searchPane = $("#griffinforms-create-form-modal-category-search-content");
     880            searchPane.find(".griffinforms-templates-category-info").hide().text("");
     881        }
     882
    665883        let debounceTimer;
    666884        $("#griffinforms-create-form-modal-templates-search-input").on("input", function() {
     
    669887            clearErrorMessage();
    670888
    671             const searchText = $(this).val().toLowerCase();
     889            const searchText = $(this).val().trim().toLowerCase();
    672890           
    673             // Only proceed with AJAX if search text is 3 or more characters
    674             if (searchText.length < 3) {
    675                 // $("#griffinforms-create-form-modal-category-search-tab").removeClass("d-none");
    676                 $("#griffinforms-create-form-modal-category-search-tab").tab("show");
     891            if (searchText.length === 0) {
     892                clearSearchHint();
    677893                const searchPane = $("#griffinforms-create-form-modal-category-search-content");
    678894                const gridRow = searchPane.children(".row").first();
    679895                gridRow.html("");
    680896                $("#griffinforms-create-form-modal-category-search-tab .badge").html(0);
     897                return;
     898            }
     899
     900            // Only proceed with AJAX if search text is 3 or more characters
     901            if (searchText.length < minSearchChars) {
     902                showSearchHint(minSearchMsg);
    681903                return;
    682904            }
     
    696918                        if (response.success) {
    697919                            clearErrorMessage();
     920                            clearSearchHint();
     921                            $("#griffinforms-create-form-modal-category-search-tab").removeClass("d-none").tab("show");
    698922                            const searchPane = $("#griffinforms-create-form-modal-category-search-content");
    699923                            const gridRow = searchPane.children(".row").first();
     
    715939               
    716940            }, 300); // Debounce delay in milliseconds
    717         });';
     941        });
     942
     943        $("#griffinforms-create-form-modal-templates-search-input").on("blur", function() {
     944            const value = $(this).val().trim();
     945            if (value.length > 0) {
     946                return;
     947            }
     948
     949            resetTemplateSearch();
     950            clearErrorMessage();
     951            clearSearchHint();
     952
     953            const allBtn = $("#griffinforms-create-form-modal-category-all-tab");
     954            if (allBtn.length) {
     955                allBtn.removeClass("d-none").tab("show");
     956                fetchTemplates("all", "All", true);
     957            }
     958        });
     959        ';
    718960    }
    719961
  • griffinforms-form-builder/trunk/config.php

    r3471571 r3473315  
    55class Config
    66{
    7     public const VERSION = '2.3.3.0';
    8     public const DB_VER = '2.3.3.0';
     7    public const VERSION = '2.3.4.0';
     8    public const DB_VER = '2.3.4.0';
    99    public const PHP_REQUIRED = '8.2';
    1010    public const WP_REQUIRED = '6.2';
  • griffinforms-form-builder/trunk/griffinforms.php

    r3471571 r3473315  
    44 * Plugin URI:        https://griffinforms.com/
    55 * Description:       A powerful and flexible form builder for WordPress. Create multi-page forms with drag-and-drop ease, custom validations, and full submission management.
    6  * Version:           2.3.3.0
     6 * Version:           2.3.4.0
    77 * Requires at least: 6.6
    88 * Requires PHP:      8.2
  • griffinforms-form-builder/trunk/readme.txt

    r3471571 r3473315  
    55Tested up to: 6.9
    66Requires PHP: 8.2
    7 Stable tag: 2.3.3.0
     7Stable tag: 2.3.4.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    174174== Changelog ==
    175175
     176= 2.3.4.0 – 2026-03-03 =
     177* Feature: Enhanced Create Form modal with improved template browsing, smoother navigation accents, and clearer search-state guidance.
     178* Feature: Blank form creation now initializes with a starter layout by default (one page, one row, one full-width column).
     179* Feature: Builder now prevents deleting the last remaining page, row, or column to preserve minimum valid form structure.
     180* Improvement: Auto-starter layout behavior is now enforced in add-page/add-row flows with server-side safeguards and client fallback protection.
     181
    176182= 2.3.3.0 – 2026-02-28 =
    177183* Fix: Isolated Gutenberg block preview theme CSS per block instance so multiple GriffinForms blocks on one page keep independent theme rendering.
     
    218224
    219225== Upgrade Notice ==
     226
     227= 2.3.4.0 =
     228Feature release focused on form creation flow and builder safety: improved Create Form UX, automatic starter layout seeding, and minimum-structure delete safeguards. Recommended update.
    220229
    221230= 2.3.3.0 =
Note: See TracChangeset for help on using the changeset viewer.