Changeset 3473315
- Timestamp:
- 03/03/2026 06:46:49 AM (9 days ago)
- Location:
- griffinforms-form-builder/trunk
- Files:
-
- 9 edited
-
admin/ajax/forms.php (modified) (2 diffs)
-
admin/app/html/widgets/presentationarea/formlayout.php (modified) (2 diffs)
-
admin/app/html/widgets/rightsidebar/itemsummary.php (modified) (7 diffs)
-
admin/css/griffinforms-forms.css (modified) (5 diffs)
-
admin/html/elements/templatecard.php (modified) (1 diff)
-
admin/html/modals/createform.php (modified) (22 diffs)
-
config.php (modified) (1 diff)
-
griffinforms.php (modified) (1 diff)
-
readme.txt (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
griffinforms-form-builder/trunk/admin/ajax/forms.php
r3448124 r3473315 6 6 { 7 7 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 } 8 59 9 60 public function exportForm() { … … 127 178 128 179 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 } 129 187 wp_send_json_success(['form_id' => $form_id]); 130 188 } else { -
griffinforms-form-builder/trunk/admin/app/html/widgets/presentationarea/formlayout.php
r3455761 r3473315 326 326 327 327 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 328 356 $response['type'] = 'success'; 329 357 // translators: %s refers to the friendly name of the parent of the item added. 330 358 $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 } 331 362 $this->refreshFormLayoutMetadata( 332 363 $this->resolveFormIdFromRequest([ … … 2015 2046 selectFormElement(htmlId); 2016 2047 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 } 2019 2054 }); 2020 2055 } -
griffinforms-form-builder/trunk/admin/app/html/widgets/rightsidebar/itemsummary.php
r3455761 r3473315 6 6 { 7 7 protected $item_type; 8 protected $delete_guard_message = ''; 8 9 9 10 public function getHtml() … … 40 41 $this->loadItem(); 41 42 } 43 44 $this->delete_guard_message = $this->getMinimumStructureDeleteGuardMessage($this->item_type, $this->item_id); 42 45 43 46 $this->itemSummary(); … … 140 143 } 141 144 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 142 155 $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' : ''), 144 157 'id' => $this->getHtmlId('delete-item-btn'), 145 158 // Translators: %s refers to the friendly name of the item type (e.g., Field, Page). 146 159 'label' => sprintf(__('Delete %s', 'griffinforms-form-builder'), $this->config->getFriendlyName($this->item_type)), 147 160 '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 152 163 ]); 153 164 } … … 268 279 269 280 $(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 } 270 288 271 289 showBtnSpinner(btnId); … … 369 387 $this->response['success'] = false; 370 388 $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; 371 397 echo wp_json_encode($this->response); 372 398 wp_die(); … … 586 612 } 587 613 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 588 654 private function findFormIdByColumnId(int $column_id): int 589 655 { … … 618 684 619 685 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 ''; 620 737 } 621 738 -
griffinforms-form-builder/trunk/admin/css/griffinforms-forms.css
r3394319 r3473315 27 27 28 28 .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; 31 35 } 32 36 33 37 #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; 36 63 } 37 64 38 65 #griffinforms-create-form-modal-templates-search-input { 39 66 border-color: #dee2e6 !important; 67 } 68 69 .gf-create-form-template-search-row { 70 padding-right: 1rem; 40 71 } 41 72 … … 46 77 47 78 #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 49 95 } 50 96 … … 79 125 position: relative; 80 126 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 } 81 189 } 82 190 … … 103 211 .griffinforms-template-thumbnail .row { 104 212 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; 105 228 } 106 229 … … 226 349 margin-bottom: -2px; 227 350 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; 228 370 } 229 371 230 372 #griffinforms-create-form-modal-tabs .nav-link.active { 231 border-color: #2271b1;232 373 color: #2271b1 !important; 233 374 } -
griffinforms-form-builder/trunk/admin/html/elements/templatecard.php
r3377809 r3473315 88 88 $html .= $badge; 89 89 $html .= '<div class="card-body p-2">'; 90 $description = $template['description'] ?? __('No description available.', 'griffinforms-form-builder'); 90 91 $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>'; 92 93 93 94 $html .= '<div class="text-center">'; -
griffinforms-form-builder/trunk/admin/html/modals/createform.php
r3377809 r3473315 57 57 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>'; 58 58 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">'; 60 60 61 61 $this->renderTabsContent(); … … 95 95 protected function templateSearch() 96 96 { 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">'; 98 98 99 99 echo '<span id="griffinforms-create-form-modal-template-search-spinner" class="spinner position-relative me-2" style="visibility: visible; bottom: 8px;"></span>'; … … 131 131 $content_html = ''; 132 132 133 echo '<div class=" d-flex align-items-start">';133 echo '<div class="gf-create-form-template-layout d-flex align-items-start">'; 134 134 echo '<div class="nav flex-column list-group small me-3" id="griffinforms-create-form-modal-category-list" role="tablist">'; 135 135 … … 151 151 152 152 $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>'; 154 158 $content_html .= '<div class="griffinforms-templates-category-info text-center mt-3 small text-muted p-3" style="display: none;"></div>'; 155 159 $content_html .='</div>'; … … 158 162 echo '</div>'; 159 163 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">'; 161 165 echo wp_kses_post($content_html); 162 166 echo '</div>'; 163 167 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; 164 191 } 165 192 … … 275 302 { 276 303 $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; 277 306 $js .= 'const getTemplatesNonce = "' . esc_js(wp_create_nonce('get_form_templates')) . '";' . PHP_EOL; 278 307 $js .= 'const formFromTemplateNonce = "' . esc_js(wp_create_nonce('form_from_template')) . '";' . PHP_EOL; … … 282 311 $js .= 'const ajaxFailMsg = "' . esc_js(__('Network request failed. Try again later.', 'griffinforms-form-builder')) . '";' . PHP_EOL; 283 312 $js .= $this->showErrorMessageJs() . PHP_EOL; 313 $js .= $this->slidingNavAccentJs() . PHP_EOL; 284 314 $js .= $this->hideErrorsOnTabSwitchJs() . PHP_EOL; 285 315 $js .= $this->fetchTemplatesJs() . PHP_EOL; … … 314 344 } 315 345 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 316 441 protected function hideErrorsOnTabSwitchJs() 317 442 { … … 332 457 const ended = {}; 333 458 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 } 334 477 335 478 function fetchTemplates(categorySlug = "all", categoryLabel = "All", isInitialLoad = false) { … … 346 489 const gridRow = pane.children(".row").first(); 347 490 gridRow.html(""); // Clear existing templates in target pane 491 if (categorySlug === "all") { 492 gridRow.append(blankFormCardHtml); 493 } 494 gridRow.append(skeletonCardsHtml()); 348 495 } 349 496 … … 366 513 }, 367 514 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 } 368 523 if (response.success) { 369 const pane = $("#griffinforms-create-form-modal-category-" + categorySlug + "-content");370 const gridRow = pane.children(".row").first();371 524 gridRow.append(response.data); // Append new templates with server-rendered previews 372 525 // Increment offset for the next fetch … … 376 529 showErrorMessage(response.data && response.data.message ? response.data.message : (response.data || "")); 377 530 } else { 378 const pane = $("#griffinforms-create-form-modal-category-" + categorySlug + "-content");379 531 pane.find(".griffinforms-templates-category-info").text(response.data.message).show(); 380 532 ended[categorySlug] = true; … … 389 541 } 390 542 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) { 400 550 const activeTab = $(".griffinforms-templates-category-tab.active"); 401 551 const categorySlug = activeTab.attr("data-gf-category") || "all"; … … 404 554 fetchTemplates(categorySlug, categoryLabel); 405 555 } 556 } 557 558 // Main infinite-load listener (templates container is the scroll owner). 559 $("#griffinforms-form-templates-container").on("scroll", function () { 560 maybeFetchMore($(this)); 406 561 }); 407 562 '; … … 441 596 protected function createBlankFormJs() { 442 597 $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 447 617 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 454 626 const data = { 455 627 action: "createBlankForm", … … 466 638 } else { 467 639 showErrorMessage(response.data); 640 blankCreateInFlight = false; 641 setBlankCreateButtonsDisabled(false); 468 642 } 469 643 }).fail(function() { 470 644 showErrorMessage("' . esc_js(__('Network request failed. Try again later.', 'griffinforms-form-builder')) . '"); 645 blankCreateInFlight = false; 646 setBlankCreateButtonsDisabled(false); 471 647 }); 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); 472 660 }); 661 662 $("#griffinforms-form-templates-container").on("click", ".griffinforms-create-blank-form-quick", function() { 663 submitBlankForm(blankFormDefaultName, ""); 664 }); 473 665 '; 474 666 return $js; … … 478 670 { 479 671 $js = ' 672 let templateCreateInFlight = false; 673 480 674 $("#griffinforms-form-templates-container").on("click", ".griffinforms-create-form-from-template", function() { 675 if (templateCreateInFlight) { 676 return; 677 } 678 templateCreateInFlight = true; 481 679 clearErrorMessage(); 482 680 let templateId = $(this).attr("data-gf-templateid"); … … 509 707 // Hide spinner 510 708 $("#griffinforms-create-form-modal-template-search-spinner").css("visibility", "hidden"); 709 templateCreateInFlight = false; 511 710 }); 512 711 }); … … 663 862 { 664 863 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 665 883 let debounceTimer; 666 884 $("#griffinforms-create-form-modal-templates-search-input").on("input", function() { … … 669 887 clearErrorMessage(); 670 888 671 const searchText = $(this).val().t oLowerCase();889 const searchText = $(this).val().trim().toLowerCase(); 672 890 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(); 677 893 const searchPane = $("#griffinforms-create-form-modal-category-search-content"); 678 894 const gridRow = searchPane.children(".row").first(); 679 895 gridRow.html(""); 680 896 $("#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); 681 903 return; 682 904 } … … 696 918 if (response.success) { 697 919 clearErrorMessage(); 920 clearSearchHint(); 921 $("#griffinforms-create-form-modal-category-search-tab").removeClass("d-none").tab("show"); 698 922 const searchPane = $("#griffinforms-create-form-modal-category-search-content"); 699 923 const gridRow = searchPane.children(".row").first(); … … 715 939 716 940 }, 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 '; 718 960 } 719 961 -
griffinforms-form-builder/trunk/config.php
r3471571 r3473315 5 5 class Config 6 6 { 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'; 9 9 public const PHP_REQUIRED = '8.2'; 10 10 public const WP_REQUIRED = '6.2'; -
griffinforms-form-builder/trunk/griffinforms.php
r3471571 r3473315 4 4 * Plugin URI: https://griffinforms.com/ 5 5 * 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.06 * Version: 2.3.4.0 7 7 * Requires at least: 6.6 8 8 * Requires PHP: 8.2 -
griffinforms-form-builder/trunk/readme.txt
r3471571 r3473315 5 5 Tested up to: 6.9 6 6 Requires PHP: 8.2 7 Stable tag: 2.3. 3.07 Stable tag: 2.3.4.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 174 174 == Changelog == 175 175 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 176 182 = 2.3.3.0 – 2026-02-28 = 177 183 * Fix: Isolated Gutenberg block preview theme CSS per block instance so multiple GriffinForms blocks on one page keep independent theme rendering. … … 218 224 219 225 == Upgrade Notice == 226 227 = 2.3.4.0 = 228 Feature release focused on form creation flow and builder safety: improved Create Form UX, automatic starter layout seeding, and minimum-structure delete safeguards. Recommended update. 220 229 221 230 = 2.3.3.0 =
Note: See TracChangeset
for help on using the changeset viewer.