diff --git a/src/CrudPanelManager.php b/src/CrudPanelManager.php index 86ca718d10..d068069c08 100644 --- a/src/CrudPanelManager.php +++ b/src/CrudPanelManager.php @@ -66,23 +66,246 @@ public function setupCrudPanel(string $controller, ?string $operation = null): C // Use provided operation or default to 'list' $operation = $operation ?? 'list'; - $crud->setOperation($operation); + + $shouldIsolate = $this->shouldIsolateOperation($controller::class, $operation); $primaryControllerRequest = $this->cruds[array_key_first($this->cruds)]->getRequest(); - if (! $crud->isInitialized() || ! $this->isOperationInitialized($controller::class, $operation)) { + + if ($crud->isInitialized() && $crud->getOperation() !== $operation && ! $shouldIsolate) { self::setActiveController($controller::class); - $crud->initialized = false; + + $crud->setOperation($operation); + $this->setupSpecificOperation($controller, $operation, $crud); + + // Mark this operation as initialized + $this->storeInitializedOperation($controller::class, $operation); + + self::unsetActiveController(); + + return $this->cruds[$controller::class]; + } + + // Check if we need to initialize this specific operation + if (! $crud->isInitialized() || ! $this->isOperationInitialized($controller::class, $operation)) { self::setActiveController($controller::class); - $controller->initializeCrudPanel($primaryControllerRequest, $crud); + + // If the panel isn't initialized at all, do full initialization + if (! $crud->isInitialized()) { + // Set the operation for full initialization + $crud->setOperation($operation); + $crud->initialized = false; + $controller->initializeCrudPanel($primaryControllerRequest, $crud); + } else { + // Panel is initialized, just setup this specific operation + // Use operation isolation for non-primary operations + if ($shouldIsolate) { + $this->setupIsolatedOperation($controller, $operation, $crud); + } else { + // Set the operation for standard setup + $crud->setOperation($operation); + $this->setupSpecificOperation($controller, $operation, $crud); + } + } + + // Mark this operation as initialized + $this->storeInitializedOperation($controller::class, $operation); + self::unsetActiveController(); $crud = $this->cruds[$controller::class]; return $this->cruds[$controller::class]; } + // If we reach here, the panel and operation are both initialized + // and the operation matches what was requested, so just return it return $this->cruds[$controller::class]; } + /** + * Determine if an operation should be isolated to prevent state interference. + * + * @param string $controller + * @param string $operation + * @return bool + */ + private function shouldIsolateOperation(string $controller, string $operation): bool + { + $currentCrud = $this->cruds[$controller] ?? null; + if (! $currentCrud) { + return false; + } + + $currentOperation = $currentCrud->getOperation(); + + // If operations don't differ, no need to isolate + if (! $currentOperation || $currentOperation === $operation) { + return false; + } + + // Check backtrace for components implementing IsolatesOperationSetup + $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 10); + + foreach ($backtrace as $trace) { + if (isset($trace['object'])) { + $object = $trace['object']; + + // If we find a component that implements the interface, use its declared behavior + if ($object instanceof \Backpack\CRUD\app\View\Components\Contracts\IsolatesOperationSetup) { + return $object->shouldIsolateOperationSetup(); + } + } + } + + return true; + } + + /** + * Setup an operation in isolation without affecting the main CRUD panel state. + * This creates a temporary context for operation setup without state interference. + * + * @param object $controller The controller instance + * @param string $operation The operation to setup + * @param CrudPanel $crud The CRUD panel instance + */ + private function setupIsolatedOperation($controller, string $operation, CrudPanel $crud): void + { + // Store the complete current state + $originalOperation = $crud->getOperation(); + $originalSettings = $crud->settings(); + $originalColumns = $crud->columns(); // Use the direct method, not operation setting + $originalRoute = $crud->route ?? null; + $originalEntityName = $crud->entity_name ?? null; + $originalEntityNamePlural = $crud->entity_name_plural ?? null; + + // Store operation-specific settings generically + $originalOperationSettings = $this->extractOperationSettings($crud, $originalOperation); + + // Temporarily setup the requested operation + $crud->setOperation($operation); + + // Use the controller's own method to setup the operation properly + $reflection = new \ReflectionClass($controller); + $method = $reflection->getMethod('setupConfigurationForCurrentOperation'); + $method->setAccessible(true); + $method->invoke($controller, $operation); + + // Completely restore the original state + $crud->setOperation($originalOperation); + + // CRITICAL: Properly restore columns by clearing and re-adding them + // This is essential to preserve list operation columns + $crud->removeAllColumns(); + foreach ($originalColumns as $column) { + $crud->addColumn($column); + } + + // Restore all original settings one by one, but skip complex objects + foreach ($originalSettings as $key => $value) { + try { + // Skip complex objects that Laravel generates dynamically + if (is_object($value) && ( + $value instanceof \Illuminate\Routing\UrlGenerator || + $value instanceof \Illuminate\Http\Request || + $value instanceof \Illuminate\Contracts\Foundation\Application || + $value instanceof \Closure || + method_exists($value, '__toString') === false + )) { + continue; + } + + $crud->set($key, $value); + } catch (\Exception $e) { + // Silently continue with restoration + } + } + + // Restore operation-specific settings generically + $this->restoreOperationSettings($crud, $originalOperation, $originalOperationSettings); + + // Restore core properties if they were changed + if ($originalRoute !== null) { + $crud->route = $originalRoute; + } + if ($originalEntityName !== null) { + $crud->entity_name = $originalEntityName; + } + if ($originalEntityNamePlural !== null) { + $crud->entity_name_plural = $originalEntityNamePlural; + } + } + + /** + * Extract all settings for a specific operation. + * + * @param CrudPanel $crud The CRUD panel instance + * @param string $operation The operation name + * @return array Array of operation-specific settings + */ + private function extractOperationSettings(CrudPanel $crud, string $operation): array + { + $settings = $crud->settings(); + $operationSettings = []; + $operationPrefix = $operation.'.'; + + foreach ($settings as $key => $value) { + if (str_starts_with($key, $operationPrefix)) { + $operationSettings[$key] = $value; + } + } + + return $operationSettings; + } + + /** + * Restore all settings for a specific operation. + * + * @param CrudPanel $crud The CRUD panel instance + * @param string $operation The operation name + * @param array $operationSettings The settings to restore + */ + private function restoreOperationSettings(CrudPanel $crud, string $operation, array $operationSettings): void + { + foreach ($operationSettings as $key => $value) { + try { + // Skip complex objects that Laravel generates dynamically + if (is_object($value) && ( + $value instanceof \Illuminate\Routing\UrlGenerator || + $value instanceof \Illuminate\Http\Request || + $value instanceof \Illuminate\Contracts\Foundation\Application || + $value instanceof \Closure || + method_exists($value, '__toString') === false + )) { + continue; + } + + $crud->set($key, $value); + } catch (\Exception $e) { + // Silently continue with restoration + } + } + } + + /** + * Setup a specific operation without reinitializing the entire CRUD panel. + * + * @param object $controller The controller instance + * @param string $operation The operation to setup + * @param CrudPanel $crud The CRUD panel instance + */ + private function setupSpecificOperation($controller, string $operation, CrudPanel $crud): void + { + // Setup the specific operation using the existing CrudController infrastructure + $crud->setOperation($operation); + + $controller->setup(); + + // Use the controller's own method to setup the operation properly + $reflection = new \ReflectionClass($controller); + $method = $reflection->getMethod('setupConfigurationForCurrentOperation'); + $method->setAccessible(true); + $method->invoke($controller, $operation); + } + /** * Check if a specific operation has been initialized for a controller. */ diff --git a/src/app/Http/Controllers/Operations/CreateOperation.php b/src/app/Http/Controllers/Operations/CreateOperation.php index e6282ff68f..d0aeb47b8c 100644 --- a/src/app/Http/Controllers/Operations/CreateOperation.php +++ b/src/app/Http/Controllers/Operations/CreateOperation.php @@ -43,6 +43,16 @@ protected function setupCreateDefaults() LifecycleHook::hookInto('list:before_setup', function () { $this->crud->addButton('top', 'create', 'view', 'crud::buttons.create'); }); + + LifecycleHook::hookInto('list:after_setup', function () { + // Check if modal form is enabled and replace the button if needed + $useModalForm = $this->crud->get('create.createButtonWithModalForm') ?? config('backpack.operations.create.createButtonWithModalForm', false); + + if ($useModalForm) { + $this->crud->removeButton('create'); + $this->crud->addButton('top', 'create', 'view', 'crud::buttons.create_in_modal', 'beginning'); + } + }); } /** @@ -54,13 +64,20 @@ public function create() { $this->crud->hasAccessOrFail('create'); + // Apply cached form setup if this is an AJAX request from a modal + if (request()->ajax() && request()->has('_modal_form_id')) { + \Backpack\CRUD\app\Library\Support\DataformCache::applySetupClosure($this->crud); + } + // prepare the fields you need to show $this->data['crud'] = $this->crud; $this->data['saveAction'] = $this->crud->getSaveAction(); $this->data['title'] = $this->crud->getTitle() ?? trans('backpack::crud.add').' '.$this->crud->entity_name; - // load the view from /resources/views/vendor/backpack/crud/ if it exists, otherwise load the one in the package - return view($this->crud->getCreateView(), $this->data); + // return the ajax response for modal forms, or the normal view for normal requests + return request()->ajax() && request()->has('_modal_form_id') ? + view('crud::components.dataform.ajax_response', $this->data) : + view($this->crud->getCreateView(), $this->data); } /** diff --git a/src/app/Http/Controllers/Operations/UpdateOperation.php b/src/app/Http/Controllers/Operations/UpdateOperation.php index f0c656092c..9d73836449 100644 --- a/src/app/Http/Controllers/Operations/UpdateOperation.php +++ b/src/app/Http/Controllers/Operations/UpdateOperation.php @@ -53,6 +53,16 @@ protected function setupUpdateDefaults() LifecycleHook::hookInto(['list:before_setup', 'show:before_setup'], function () { $this->crud->addButton('line', 'update', 'view', 'crud::buttons.update', 'end'); }); + + LifecycleHook::hookInto(['list:after_setup', 'show:after_setup'], function () { + // Check if modal form is enabled and replace the button if needed + $useModalForm = $this->crud->get('update.updateButtonWithModalForm') ?? config('backpack.operations.update.updateButtonWithModalForm', false); + + if ($useModalForm) { + $this->crud->removeButton('update'); + $this->crud->addButton('line', 'update', 'view', 'crud::buttons.update_in_modal', 'end'); + } + }); } /** @@ -68,6 +78,11 @@ public function edit($id) // get entry ID from Request (makes sure its the last ID for nested resources) $id = $this->crud->getCurrentEntryId() ?? $id; + // Apply cached form setup if this is an AJAX request from a modal + if (request()->ajax() && request()->has('_modal_form_id')) { + \Backpack\CRUD\app\Library\Support\DataformCache::applySetupClosure($this->crud); + } + // register any Model Events defined on fields $this->crud->registerFieldEvents(); @@ -82,7 +97,9 @@ public function edit($id) $this->data['id'] = $id; // load the view from /resources/views/vendor/backpack/crud/ if it exists, otherwise load the one in the package - return view($this->crud->getEditView(), $this->data); + return request()->ajax() && request()->has('_modal_form_id') ? + view('crud::components.dataform.ajax_response', $this->data) : + view($this->crud->getEditView(), $this->data); } /** diff --git a/src/app/Library/CrudPanel/Traits/Read.php b/src/app/Library/CrudPanel/Traits/Read.php index fa30de949d..4175e9f8ff 100644 --- a/src/app/Library/CrudPanel/Traits/Read.php +++ b/src/app/Library/CrudPanel/Traits/Read.php @@ -275,8 +275,14 @@ public function getDefaultPageLength() */ public function addCustomPageLengthToPageLengthMenu() { - $values = $this->getOperationSetting('pageLengthMenu')[0]; - $labels = $this->getOperationSetting('pageLengthMenu')[1]; + $pageLengthMenu = $this->getOperationSetting('pageLengthMenu'); + + if (is_null($pageLengthMenu)) { + return; + } + + $values = $pageLengthMenu[0]; + $labels = $pageLengthMenu[1]; if (array_search($this->getDefaultPageLength(), $values) === false) { for ($i = 0; $i < count($values); $i++) { diff --git a/src/app/Library/CrudPanel/Traits/Search.php b/src/app/Library/CrudPanel/Traits/Search.php index c3c253f384..bbfe4a1d6e 100644 --- a/src/app/Library/CrudPanel/Traits/Search.php +++ b/src/app/Library/CrudPanel/Traits/Search.php @@ -4,7 +4,7 @@ use Backpack\CRUD\ViewNamespaces; use Carbon\Carbon; -use Validator; +use Illuminate\Support\Facades\Validator; trait Search { @@ -275,14 +275,14 @@ public function getRowViews($entry, $rowNumber = false) ->render(); } - // add the bulk actions checkbox to the first column - if ($this->getOperationSetting('bulkActions')) { + // add the bulk actions checkbox to the first column - but only if we have columns + if ($this->getOperationSetting('bulkActions') && ! empty($row_items)) { $bulk_actions_checkbox = \View::make('crud::columns.inc.bulk_actions_checkbox', ['entry' => $entry])->render(); $row_items[0] = $bulk_actions_checkbox.$row_items[0]; } - // add the details_row button to the first column - if ($this->getOperationSetting('detailsRow')) { + // add the details_row button to the first column - but only if we have columns + if ($this->getOperationSetting('detailsRow') && ! empty($row_items)) { $details_row_button = \View::make('crud::columns.inc.details_row_button') ->with('crud', $this) ->with('entry', $entry) @@ -291,7 +291,7 @@ public function getRowViews($entry, $rowNumber = false) $row_items[0] = $details_row_button.$row_items[0]; } - if ($this->getResponsiveTable()) { + if ($this->getResponsiveTable() && ! empty($row_items)) { $responsiveTableTrigger = '
'; $row_items[0] = $responsiveTableTrigger.$row_items[0]; } @@ -398,16 +398,18 @@ public function getEntriesAsJsonForDatatables($entries, $totalRows, $filteredRow { $rows = []; - foreach ($entries as $row) { + foreach ($entries as $index => $row) { $rows[] = $this->getRowViews($row, $startIndex === false ? false : ++$startIndex); } - return [ + $result = [ 'draw' => (isset($this->getRequest()['draw']) ? (int) $this->getRequest()['draw'] : 0), 'recordsTotal' => $totalRows, 'recordsFiltered' => $filteredRows, 'data' => $rows, ]; + + return $result; } /** diff --git a/src/app/Library/Support/DataformCache.php b/src/app/Library/Support/DataformCache.php new file mode 100644 index 0000000000..6661bc1d3c --- /dev/null +++ b/src/app/Library/Support/DataformCache.php @@ -0,0 +1,218 @@ +cachePrefix = 'dataform_config_'; + $this->cacheDuration = 60; // 1 hour + } + + /** + * Cache setup closure for a dataform component. + * + * @param string $formId The form ID to use as cache key + * @param string $controllerClass The controller class + * @param array $fieldsConfig The field configuration after setup was applied + * @param string|null $name The element name + * @param CrudPanel $crud The CRUD panel instance to update with form_id + * @return bool Whether the operation was successful + */ + public function cacheForComponent(string $formId, string $controllerClass, array $fieldsConfig, ?string $name = null, ?CrudPanel $crud = null): bool + { + if (empty($fieldsConfig)) { + return false; + } + + $cruds = CrudManager::getCrudPanels(); + $parentCrud = reset($cruds); + + $parentEntry = null; + $parentController = null; + + if ($parentCrud && $parentCrud->getCurrentEntry()) { + $parentEntry = $parentCrud->getCurrentEntry(); + $parentController = $parentCrud->controller; + } + + // Store metadata in cache (even without parent entry for standalone modal forms) + $this->store( + $formId, + $controllerClass, + $parentController, + $parentEntry, + $name + ); + + // Store the field configuration in Laravel cache (persists across requests) + \Cache::put($this->cachePrefix.$formId.'_fields', $fieldsConfig, now()->addMinutes($this->cacheDuration)); + + // Set the form_id in the CRUD panel if provided + if ($crud) { + $crud->set('form.form_id', $formId); + } + + return true; + } + + public static function applyAndStoreSetupClosure( + string $formId, + string $controllerClass, + \Closure $setupClosure, + ?string $name = null, + ?CrudPanel $crud = null, + $parentEntry = null + ): bool { + $instance = new self(); + // Apply the setup closure to the CrudPanel instance + if ($instance->applyClosure($crud, $controllerClass, $setupClosure, $parentEntry)) { + // Capture the resulting field configuration after setup + $fieldsAfterSetup = []; + foreach ($crud->fields() as $fieldName => $field) { + $fieldsAfterSetup[$fieldName] = $field; + } + + // Cache the field configuration (not the closure, since it won't persist across requests) + $cached = $instance->cacheForComponent($formId, $controllerClass, $fieldsAfterSetup, $name, $crud); + + return $cached; + } + + return false; + } + + /** + * Apply cached setup to a CRUD instance using the request's form_id. + * + * @param CrudPanel $crud The CRUD panel instance + * @return bool Whether the operation was successful + */ + public static function applySetupClosure(CrudPanel $crud): bool + { + $instance = new self(); + // Check if the request has a _modal_form_id parameter + $formId = request('_modal_form_id'); + + if (! $formId) { + return false; + } + + return $instance->apply($formId, $crud); + } + + /** + * Apply a setup closure to a CrudPanel instance. + * + * @param CrudPanel $crud The CRUD panel instance + * @param string $controllerClass The controller class + * @param \Closure $setupClosure The setup closure + * @param mixed $entry The entry to pass to the setup closure + * @return bool Whether the operation was successful + */ + private function applyClosure(CrudPanel $crud, string $controllerClass, \Closure $setupClosure, $entry = null): bool + { + $originalSetup = $setupClosure; + $modifiedSetup = function ($crud, $entry) use ($originalSetup, $controllerClass) { + CrudManager::setActiveController($controllerClass); + + // Run the original closure + return ($originalSetup)($crud, $entry); + }; + + try { + // Execute the modified closure + ($modifiedSetup)($crud, $entry); + + return true; + } catch (\Exception $e) { + return false; + } finally { + // Clean up + CrudManager::unsetActiveController(); + } + } + + /** + * Prepare dataform data for storage in the cache. + * + * @param string $controllerClass The controller class + * @param string $parentController The parent controller + * @param mixed $parentEntry The parent entry + * @param string|null $elementName The element name + * @return array The data to be cached + */ + protected function prepareDataForStorage(...$args): array + { + [$controllerClass, $parentController, $parentEntry, $elementName] = $args; + + return [ + 'controller' => $controllerClass, + 'parentController' => $parentController, + 'parent_entry' => $parentEntry, + 'element_name' => $elementName, + 'operations' => $parentController ? CrudManager::getInitializedOperations($parentController) : [], + ]; + } + + /** + * Apply data from the cache to configure a dataform. + * + * @param array $cachedData The cached data + * @param CrudPanel $crud The CRUD panel instance + * @return bool Whether the operation was successful + */ + protected function applyFromCache($cachedData, ...$args): bool + { + [$crud] = $args; + + try { + // Initialize operations for the parent controller (if it exists) + if (! empty($cachedData['parentController'])) { + $this->initializeOperations($cachedData['parentController'], $cachedData['operations']); + } + $entry = $cachedData['parent_entry'] ?? null; + + // Get the form_id from the cached data + $formId = $crud->get('form.form_id') ?? request()->input('_form_id'); + + // Retrieve the field configuration from Laravel cache + if ($formId) { + $fieldsConfig = \Cache::get($this->cachePrefix.$formId.'_fields'); + + if ($fieldsConfig && is_array($fieldsConfig)) { + // Clear all existing fields + $crud->setOperationSetting('fields', []); + + // Restore the cached field configuration + foreach ($fieldsConfig as $fieldName => $field) { + $crud->addField($field); + } + + return true; + } + } + + return false; + } catch (\Exception $e) { + return false; + } + } + + /** + * Initialize operations for a parent controller. + */ + private function initializeOperations(string $parentController, $operations): void + { + $parentCrud = CrudManager::setupCrudPanel($parentController); + + foreach ($operations as $operation) { + $parentCrud->initialized = false; + CrudManager::setupCrudPanel($parentController, $operation); + } + } +} diff --git a/src/app/View/Components/Contracts/IsolatesOperationSetup.php b/src/app/View/Components/Contracts/IsolatesOperationSetup.php new file mode 100644 index 0000000000..7f905616bd --- /dev/null +++ b/src/app/View/Components/Contracts/IsolatesOperationSetup.php @@ -0,0 +1,29 @@ +crud = CrudManager::setupCrudPanel($controller, $operation); + $this->crud = CrudManager::setupCrudPanel($controller, $this->formOperation); + + if ($this->crud->getOperation() !== $this->formOperation) { + $this->crud->setOperation($this->formOperation); + } $this->crud->setAutoFocusOnFirstField($this->focusOnFirstField); - if ($this->entry && $this->operation === 'update') { - $this->action = $action ?? url($this->crud->route.'/'.$this->entry->getKey()); - $this->method = 'put'; + if ($this->entry && $this->formOperation === 'update') { + $this->formAction = $formAction ?? url($this->crud->route.'/'.$this->entry->getKey()); + $this->formMethod = 'put'; $this->crud->entry = $this->entry; $this->crud->setOperationSetting('fields', $this->crud->getUpdateFields()); } else { - $this->action = $action ?? url($this->crud->route); + $this->formAction = $formAction ?? url($this->crud->route); } - $this->hasUploadFields = $this->crud->hasUploadFields($operation, $this->entry?->getKey()); - $this->id = $id.md5($this->action.$this->operation.$this->method.$this->controller); + + $this->hasUploadFields = $this->crud->hasUploadFields($this->formOperation, $this->entry?->getKey()); + $this->id = $id.md5($this->formAction.$this->formOperation.$this->formMethod.$this->controller); if ($this->setup) { - $this->applySetupClosure(); + $parentEntry = $this->getParentCrudEntry(); + call_user_func($this->setup, $this->crud, $parentEntry); } + // Reset the active controller CrudManager::unsetActiveController(); } - public function applySetupClosure(): bool + private function getParentCrudEntry() { - $originalSetup = $this->setup; - $controllerClass = $this->controller; - $crud = $this->crud; - $entry = $this->entry; - - $modifiedSetup = function ($crud, $entry) use ($originalSetup, $controllerClass) { - CrudManager::setActiveController($controllerClass); - - // Run the original closure - return ($originalSetup)($crud, $entry); - }; - - try { - // Execute the modified closure - ($modifiedSetup)($crud, $entry); - - return true; - } finally { - // Clean up - CrudManager::unsetActiveController(); + $cruds = CrudManager::getCrudPanels(); + $parentCrud = reset($cruds); + + if ($parentCrud && $parentCrud->getCurrentEntry()) { + CrudManager::storeInitializedOperation( + $parentCrud->controller, + $parentCrud->getCurrentOperation() + ); + + return $parentCrud->getCurrentEntry(); } + + return null; } /** @@ -90,16 +98,16 @@ public function applySetupClosure(): bool public function render() { // Store the current form ID in the service container for form-aware old() helper - app()->instance('backpack.current_form_id', $this->id); + app()->instance('backpack.current_form_modal_id', $this->id); return view('crud::components.dataform.form', [ 'crud' => $this->crud, 'saveAction' => $this->crud->getSaveAction(), 'id' => $this->id, 'name' => $this->name, - 'operation' => $this->operation, - 'action' => $this->action, - 'method' => $this->method, + 'formOperation' => $this->formOperation, + 'formAction' => $this->formAction, + 'formMethod' => $this->formMethod, 'hasUploadFields' => $this->hasUploadFields, 'entry' => $this->entry, ]); diff --git a/src/app/View/Components/DataformModal.php b/src/app/View/Components/DataformModal.php new file mode 100644 index 0000000000..5b33f0c96f --- /dev/null +++ b/src/app/View/Components/DataformModal.php @@ -0,0 +1,112 @@ +route === null) { + \Backpack\CRUD\CrudManager::setActiveController($controller); + $tempCrud = \Backpack\CRUD\CrudManager::setupCrudPanel($controller, $this->formOperation); + $this->route = $tempCrud->route; + \Backpack\CRUD\CrudManager::unsetActiveController(); + } + + // keep backwards compatible behavior for resolving route when not provided + $this->formAction = $this->formAction ?? url($this->route); + + // Generate the SAME hashed form ID that the Dataform component uses + $this->hashedFormId = $this->id.md5($this->formAction.$this->formOperation.'post'.$this->controller); + + // Cache the setup closure if provided (for retrieval during AJAX request) + if ($this->setup instanceof \Closure) { + $this->cacheSetupClosure(); + } + + // DO NOT call parent::__construct() because we don't want to initialize + // the CRUD panel on page load - the form will be loaded via AJAX + + if ($this->entry && $this->formOperation === 'update') { + // Use the resolved action (base route) to build the edit URL for the entry + $this->formUrl = url($this->formAction.'/'.$this->entry->getKey().'/edit'); + } + } + + /** + * Cache the setup closure for later retrieval during AJAX form load. + */ + protected function cacheSetupClosure(): void + { + // Create a temporary CRUD instance to apply and cache the setup + \Backpack\CRUD\CrudManager::setActiveController($this->controller); + $tempCrud = \Backpack\CRUD\CrudManager::setupCrudPanel($this->controller, $this->formOperation); + + // Apply and cache the setup closure using the HASHED ID + \Backpack\CRUD\app\Library\Support\DataformCache::applyAndStoreSetupClosure( + $this->hashedFormId, // Use the hashed ID that matches what Dataform component generates + $this->controller, + $this->setup, + null, + $tempCrud, + null + ); + + \Backpack\CRUD\CrudManager::unsetActiveController(); + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View|\Closure|string + */ + public function render() + { + // We don't need $crud here because the modal loads the form via AJAX + // The CRUD panel will be initialized when the AJAX request is made + return view('crud::components.dataform.modal-form', [ + 'id' => $this->id, + 'formOperation' => $this->formOperation, + 'formUrl' => $this->formUrl, + 'hasUploadFields' => $this->hasUploadFields, + 'refreshDatatable' => $this->refreshDatatable, + 'formAction' => $this->formAction, + 'formMethod' => $this->formMethod, + 'title' => $this->title, + 'classes' => $this->classes, + 'hashedFormId' => $this->hashedFormId, + 'controller' => $this->controller, + 'route' => $this->route, // Pass the route for building URLs in the template + ]); + } +} diff --git a/src/app/View/Components/Datatable.php b/src/app/View/Components/Datatable.php index b906c1be04..66664be734 100644 --- a/src/app/View/Components/Datatable.php +++ b/src/app/View/Components/Datatable.php @@ -4,13 +4,23 @@ use Backpack\CRUD\app\Library\CrudPanel\CrudPanel; use Backpack\CRUD\app\Library\Support\DatatableCache; +use Backpack\CRUD\app\View\Components\Contracts\IsolatesOperationSetup; use Backpack\CRUD\CrudManager; use Illuminate\View\Component; -class Datatable extends Component +class Datatable extends Component implements IsolatesOperationSetup { protected string $tableId; + /** + * Datatables do NOT isolate their operation setup. + * They manage their own operation state independently. + */ + public function shouldIsolateOperationSetup(): bool + { + return false; + } + public function __construct( private string $controller, private ?CrudPanel $crud = null, @@ -23,10 +33,13 @@ public function __construct( $this->crud ??= CrudManager::setupCrudPanel($controller, 'list'); + if ($this->crud->getOperation() !== 'list') { + $this->crud->setOperation('list'); + } + $this->tableId = $this->generateTableId(); - if ($this->setup) { - // Apply the configuration using DatatableCache + if ($this->setup) { // Apply the configuration using DatatableCache DatatableCache::applyAndStoreSetupClosure( $this->tableId, $this->controller, @@ -38,7 +51,12 @@ public function __construct( } if (! $this->crud->has('list.datatablesUrl')) { - $this->crud->set('list.datatablesUrl', $this->crud->getRoute()); + $route = $this->crud->getRoute(); + // If route is not set, generate it from the controller + if (empty($route)) { + $route = action([$this->controller, 'index']); + } + $this->crud->set('list.datatablesUrl', $route); } // Reset the active controller @@ -77,6 +95,7 @@ public function render() 'crud' => $this->crud, 'modifiesUrl' => $this->modifiesUrl, 'tableId' => $this->tableId, + 'datatablesUrl' => url($this->crud->get('list.datatablesUrl')), ]); } } diff --git a/src/config/backpack/operations/create.php b/src/config/backpack/operations/create.php index 2bdef21f18..66c4f267a4 100644 --- a/src/config/backpack/operations/create.php +++ b/src/config/backpack/operations/create.php @@ -21,6 +21,9 @@ // when the page loads, put the cursor on the first input? 'autoFocusOnFirstField' => true, + // when enabled, the create button will open a modal form instead of the create page + 'createButtonWithModalForm' => false, + // Where do you want to redirect the user by default, save? // options: save_and_back, save_and_edit, save_and_new 'defaultSaveAction' => 'save_and_back', diff --git a/src/config/backpack/operations/update.php b/src/config/backpack/operations/update.php index 27fe127475..784bb17c6e 100644 --- a/src/config/backpack/operations/update.php +++ b/src/config/backpack/operations/update.php @@ -21,6 +21,9 @@ // when the page loads, put the cursor on the first input? 'autoFocusOnFirstField' => true, + // when enabled, the update button will open a modal form instead of the edit page + 'updateButtonWithModalForm' => false, + // Where do you want to redirect the user by default, save? // options: save_and_back, save_and_edit, save_and_new 'defaultSaveAction' => 'save_and_back', diff --git a/src/resources/assets/css/common.css b/src/resources/assets/css/common.css index 4c6a13ac23..77d8d7c79d 100644 --- a/src/resources/assets/css/common.css +++ b/src/resources/assets/css/common.css @@ -2,6 +2,7 @@ --table-row-hover: #f2f1ff; --select2-selected-item-background: #7c69ef; --select2-selected-item-color: #fff; + --btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e"); } .sidebar .nav-dropdown-items .nav-dropdown { @@ -123,6 +124,13 @@ div[id$="_wrapper"] .dt-processing { z-index: 999999 !important; } +/* Ensure Select2 dropdowns are visible and properly positioned in all contexts */ +/* Bootstrap modals use z-index 1050-1055, so Select2 needs to be higher */ +.select2-in-modal-repeatable .select2-dropdown { + z-index: 1060 !important; + position: fixed !important; +} + .navbar-filters { min-height: 25px; border-radius: 0; @@ -254,7 +262,9 @@ div[id$="_wrapper"] .dt-processing { .modal .details-control { display: none; } - +.modal .btn-close, .modal .close { + background: transparent var(--btn-close-bg) center / .75rem auto no-repeat; +} .dtr-bs-modal .modal-body { padding: 0; } diff --git a/src/resources/views/crud/buttons/create.blade.php b/src/resources/views/crud/buttons/create.blade.php index 483f62d703..1056708fdf 100644 --- a/src/resources/views/crud/buttons/create.blade.php +++ b/src/resources/views/crud/buttons/create.blade.php @@ -1,4 +1,5 @@ @if ($crud->hasAccess('create')) + {{-- Regular create button that redirects to create page --}} {{ trans('backpack::crud.add') }} {{ $crud->entity_name }} diff --git a/src/resources/views/crud/buttons/create_in_modal.blade.php b/src/resources/views/crud/buttons/create_in_modal.blade.php new file mode 100644 index 0000000000..620a9f2a47 --- /dev/null +++ b/src/resources/views/crud/buttons/create_in_modal.blade.php @@ -0,0 +1,27 @@ +@if ($crud->hasAccess('create')) + @php + $controllerClass = get_class(app('request')->route()->getController()); + @endphp + + {{-- Create button that opens modal form --}} + + + {{-- Include the modal form component --}} + +@endif diff --git a/src/resources/views/crud/buttons/create_modal_form.blade.php b/src/resources/views/crud/buttons/create_modal_form.blade.php new file mode 100644 index 0000000000..620a9f2a47 --- /dev/null +++ b/src/resources/views/crud/buttons/create_modal_form.blade.php @@ -0,0 +1,27 @@ +@if ($crud->hasAccess('create')) + @php + $controllerClass = get_class(app('request')->route()->getController()); + @endphp + + {{-- Create button that opens modal form --}} + + + {{-- Include the modal form component --}} + +@endif diff --git a/src/resources/views/crud/buttons/update.blade.php b/src/resources/views/crud/buttons/update.blade.php index 3188c1898a..88b2d485d8 100644 --- a/src/resources/views/crud/buttons/update.blade.php +++ b/src/resources/views/crud/buttons/update.blade.php @@ -1,28 +1,25 @@ @if ($crud->hasAccess('update', $entry)) - @if (!$crud->model->translationEnabled()) - - {{-- Single edit button --}} - - {{ trans('backpack::crud.edit') }} - - - @else - - {{-- Edit button group --}} -
- - {{ trans('backpack::crud.edit') }} - - - -
- - @endif -@endif + {{-- Regular update button that redirects to edit page --}} + @if (!$crud->model->translationEnabled()) + {{-- Single edit button --}} + + {{ trans('backpack::crud.edit') }} + + @else + {{-- Edit button group --}} +
+ + {{ trans('backpack::crud.edit') }} + + + +
+ @endif +@endif \ No newline at end of file diff --git a/src/resources/views/crud/buttons/update_in_modal.blade.php b/src/resources/views/crud/buttons/update_in_modal.blade.php new file mode 100644 index 0000000000..c0f52f3883 --- /dev/null +++ b/src/resources/views/crud/buttons/update_in_modal.blade.php @@ -0,0 +1,49 @@ +@if ($crud->hasAccess('update', $entry)) + @php + $controllerClass = get_class(app('request')->route()->getController()); + @endphp + + {{-- Update button that opens modal form --}} + @if (!$crud->model->translationEnabled()) + + @else + {{-- Edit button group for translated models --}} +
+ + + +
+ @endif + + {{-- Include the modal form component --}} + +@endif diff --git a/src/resources/views/crud/buttons/update_modal_form.blade.php b/src/resources/views/crud/buttons/update_modal_form.blade.php new file mode 100644 index 0000000000..c0f52f3883 --- /dev/null +++ b/src/resources/views/crud/buttons/update_modal_form.blade.php @@ -0,0 +1,49 @@ +@if ($crud->hasAccess('update', $entry)) + @php + $controllerClass = get_class(app('request')->route()->getController()); + @endphp + + {{-- Update button that opens modal form --}} + @if (!$crud->model->translationEnabled()) + + @else + {{-- Edit button group for translated models --}} +
+ + + +
+ @endif + + {{-- Include the modal form component --}} + +@endif diff --git a/src/resources/views/crud/components/dataform/ajax_response.blade.php b/src/resources/views/crud/components/dataform/ajax_response.blade.php new file mode 100644 index 0000000000..9396c63636 --- /dev/null +++ b/src/resources/views/crud/components/dataform/ajax_response.blade.php @@ -0,0 +1,44 @@ +@php +\Alert::flush(); + +$loadedAssets = json_decode($parentLoadedAssets ?? '[]', true); + +//mark parent crud assets as loaded. +foreach($loadedAssets as $asset) { + Basset::markAsLoaded($asset); +} + +@endphp + +
+{!! csrf_field() !!} +@include('crud::components.dataform.form_content', ['fields' => $crud->fields(), 'action' => 'edit', 'inlineCreate' => true, 'initFields' => false, 'id' => request('_form_id')]) + {{-- This makes sure that all field assets are loaded. --}} +
{{ json_encode(Basset::loaded()) }}
+
+ + +@foreach (app('widgets')->toArray() as $currentWidget) +@php + $currentWidget = \Backpack\CRUD\app\Library\Widget::add($currentWidget); +@endphp + @if($currentWidget->getAttribute('inline')) + @include($currentWidget->getFinalViewPath(), ['widget' => $currentWidget->toArray()]) + @endif +@endforeach + +@stack('before_scripts') + +@stack('crud_fields_scripts') + +@stack('crud_fields_styles') + +@stack('after_scripts') + +@stack('after_styles') + + diff --git a/src/resources/views/crud/components/dataform/common_js.blade.php b/src/resources/views/crud/components/dataform/common_js.blade.php new file mode 100644 index 0000000000..e2cdc2b69c --- /dev/null +++ b/src/resources/views/crud/components/dataform/common_js.blade.php @@ -0,0 +1,124 @@ +@verbatim +if (typeof window.initializeFieldsWithJavascript === 'undefined') { + window.initializeFieldsWithJavascript = function(container) { + var selector; + if (container instanceof jQuery) { + selector = container; + } else { + selector = $(container); + } + + var fieldsToInit = selector.find("[data-init-function]").not("[data-initialized=true]"); + + fieldsToInit.each(function () { + var element = $(this); + var functionName = element.data('init-function'); + + if (typeof window[functionName] === "function") { + try { + window[functionName](element); + element.attr('data-initialized', 'true'); + } catch (error) { + element.attr('data-initialized', 'true'); + console.error('[FieldInit] Error initializing field with function ' + functionName + ':', error); + } + } + }); + }; +} + +if (!window._select2FocusFixInstalled) { + document.addEventListener('focusin', function(e) { + if (e.target.classList.contains('select2-search__field') || + e.target.closest('.select2-container') || + e.target.closest('.select2-dropdown')) { + e.stopImmediatePropagation(); + } + }, true); + + window._select2FocusFixInstalled = true; +} + +/** + * Auto-discover first focusable input + * @param {jQuery} form + * @return {jQuery} + */ +function getFirstFocusableField(form) { + return form.find('input, select, textarea, button') + .not('.close') + .not('[disabled]') + .filter(':visible:first'); +} + +/** + * + * @param {jQuery} firstField + */ +function triggerFocusOnFirstInputField(firstField) { + if (firstField.hasClass('select2-hidden-accessible')) { + return handleFocusOnSelect2Field(firstField); + } + + firstField.trigger('focus'); +} + +/** + * 1- Make sure no other select2 input is open in other field to focus on the right one + * 2- Check until select2 is initialized + * 3- Open select2 + * + * @param {jQuery} firstField + */ +function handleFocusOnSelect2Field(firstField){ + firstField.select2('focus'); +} + +/* +* Hacky fix for a bug in select2 with jQuery 3.6.0's new nested-focus "protection" +* see: https://github.com/select2/select2/issues/5993 +* see: https://github.com/jquery/jquery/issues/4382 +* +*/ +$(document).on('select2:open', () => { + setTimeout(() => document.querySelector('.select2-container--open .select2-search__field').focus(), 100); +}); + +// When Select2 opens inside a repeatable row that is itself inside a modal, +// add a specific class to the open container so CSS/positioning logic can target it. +// Also remove the class on close. +$(document).on('select2:open', function(e) { + // The event target will be the original select element + try { + var $select = $(e.target); + var $repeatable = $select.closest('.repeatable-element'); + var $modal = $select.closest('.modal'); + + if ($repeatable.length && $modal.length) { + // Wait briefly for Select2 to render the dropdown container + setTimeout(function() { + var $openContainer = $('.select2-container--open'); + $openContainer.addClass('select2-in-modal-repeatable'); + }, 0); + } + } catch (err) { + // fail silently + } +}); + +$(document).on('select2:close', function(e) { + try { + var $select = $(e.target); + var $repeatable = $select.closest('.repeatable-element'); + var $modal = $select.closest('.modal'); + + if ($repeatable.length && $modal.length) { + // remove the class from any open containers + $('.select2-container--open').removeClass('select2-in-modal-repeatable'); + } + } catch (err) { + // fail silently + } +}); + +@endverbatim diff --git a/src/resources/views/crud/components/dataform/form.blade.php b/src/resources/views/crud/components/dataform/form.blade.php index 023f2bb8cc..cfc0c620b6 100644 --- a/src/resources/views/crud/components/dataform/form.blade.php +++ b/src/resources/views/crud/components/dataform/form.blade.php @@ -1,20 +1,18 @@
@include('crud::inc.grouped_errors', ['id' => $id]) -
{!! csrf_field() !!} - - @if($method !== 'post') - @method($method) + @if($formMethod !== 'post') + @method($formMethod) @endif {{-- Include the form fields --}} - @include('crud::form_content', ['fields' => $crud->fields(), 'action' => $operation, 'id' => $id]) + @include('crud::form_content', ['fields' => $crud->fields(), 'action' => $formOperation, 'id' => $id]) {{-- This makes sure that all field assets are loaded. --}}
{{ json_encode(Basset::loaded()) }}
diff --git a/src/resources/views/crud/components/dataform/form_content.blade.php b/src/resources/views/crud/components/dataform/form_content.blade.php new file mode 100644 index 0000000000..a1abea3704 --- /dev/null +++ b/src/resources/views/crud/components/dataform/form_content.blade.php @@ -0,0 +1,28 @@ + + + +{{-- See if we're using tabs --}} +@if ($crud->tabsEnabled() && count($crud->getTabs())) + @include('crud::inc.show_tabbed_fields', ['fields' => $crud->fields()]) + +@else +
+
+ @include('crud::inc.show_fields', ['fields' => $crud->fields()]) +
+
+@endif + +@foreach (app('widgets')->toArray() as $currentWidget) +@php + $currentWidget = \Backpack\CRUD\app\Library\Widget::add($currentWidget); +@endphp + @if($currentWidget->getAttribute('inline')) + @include($currentWidget->getFinalViewPath(), ['widget' => $currentWidget->toArray()]) + @endif +@endforeach + + +@push('before_scripts') + @include('crud::inc.form_fields_script') +@endpush diff --git a/src/resources/views/crud/components/dataform/modal-form-scripts.blade.php b/src/resources/views/crud/components/dataform/modal-form-scripts.blade.php new file mode 100644 index 0000000000..37515215f8 --- /dev/null +++ b/src/resources/views/crud/components/dataform/modal-form-scripts.blade.php @@ -0,0 +1,612 @@ + diff --git a/src/resources/views/crud/components/dataform/modal-form.blade.php b/src/resources/views/crud/components/dataform/modal-form.blade.php new file mode 100644 index 0000000000..0c565726bc --- /dev/null +++ b/src/resources/views/crud/components/dataform/modal-form.blade.php @@ -0,0 +1,43 @@ +{{-- Modal HTML (initially hidden from DOM) --}} +{{-- compute form load route inline where used --}} +@push('after_styles') @if (request()->ajax()) @endpush @endif +@if (!request()->ajax()) @endpush @endif +@push('after_scripts') @if (request()->ajax()) @endpush @endif +
+ +
+@if (!request()->ajax()) @endpush @endif +@push('after_scripts') @if (request()->ajax()) @endpush @endif + @include('crud::components.dataform.modal-form-scripts') +@if (!request()->ajax()) @endpush @endif diff --git a/src/resources/views/crud/components/datatable/datatable.blade.php b/src/resources/views/crud/components/datatable/datatable.blade.php index 953b79440e..0ad285d9cb 100644 --- a/src/resources/views/crud/components/datatable/datatable.blade.php +++ b/src/resources/views/crud/components/datatable/datatable.blade.php @@ -33,7 +33,7 @@ @include('crud::inc.filters_navbar', ['componentId' => $tableId]) @endif
-getOperationSetting('lineButtonsAsDropdown') }}" data-line-buttons-as-dropdown-minimum="{{ (int) $crud->getOperationSetting('lineButtonsAsDropdownMinimum') }}" data-line-buttons-as-dropdown-show-before-dropdown="{{ (int) $crud->getOperationSetting('lineButtonsAsDropdownShowBefore') }}" - data-url-start="{{ url($crud->getOperationSetting('datatablesUrl')) }}" + data-url-start="{{ $datatablesUrl }}" data-responsive-table="{{ $crud->getResponsiveTable() ? 'true' : 'false' }}" data-persistent-table="{{ $crud->getPersistentTable() ? 'true' : 'false' }}" data-persistent-table-slug="{{ Str::slug($crud->getOperationSetting('datatablesUrl')) }}" diff --git a/src/resources/views/crud/components/datatable/datatable_logic.blade.php b/src/resources/views/crud/components/datatable/datatable_logic.blade.php index ca64d1def6..d6d9635e2f 100644 --- a/src/resources/views/crud/components/datatable/datatable_logic.blade.php +++ b/src/resources/views/crud/components/datatable/datatable_logic.blade.php @@ -140,7 +140,8 @@ functionsToRunOnDataTablesDrawEvent: [], window.crud.executeFunctionByName = window.crud.defaultTableConfig.executeFunctionByName; window.crud.updateUrl = window.crud.defaultTableConfig.updateUrl; -window.crud.initializeTable = function(tableId, customConfig = {}) { +window.crud.initializeTable = function(tableId, customConfig = {}) { + // Create a table-specific configuration if (!window.crud.tableConfigs[tableId]) { window.crud.tableConfigs[tableId] = {}; @@ -342,16 +343,54 @@ functionsToRunOnDataTablesDrawEvent: [], target: '.dtr-control', renderer: function(api, rowIdx, columns) { var data = $.map(columns, function(col, i) { + // Safety check for column index + if (!col || col.columnIndex === undefined || col.columnIndex === null) { + return ''; + } + + // Check if column is explicitly disabled for modal + var isModalDisabled = false; + + try { + var headerCell = table.column(col.columnIndex).header(); + isModalDisabled = $(headerCell).data('visible-in-modal') === false || $(headerCell).data('visible-in-modal') === 'false'; + } catch (e) { + // Column header not accessible - default to showing the column + isModalDisabled = false; + } + + // Skip columns that are explicitly disabled for modal + if (isModalDisabled) { + return ''; + } + // Use the table instance from the API var table = api.table().context[0].oInstance; var tableId = table.attr('id'); - var columnHeading = window.crud.tables[tableId].columns().header()[col.columnIndex]; + + // Check if we're in a modal context + if (table.closest('.modal').length > 0) { + return ''; + } + + var columnHeading; + if (window.crud?.tables?.[tableId]?.columns) { + columnHeading = window.crud.tables[tableId].columns().header()[col.columnIndex]; + } else { + // Fallback: get column heading directly from table header + columnHeading = table.find('thead th').eq(col.columnIndex)[0]; + } if ($(columnHeading).attr('data-visible-in-modal') == 'false') { return ''; } - if (col.data.indexOf('crud_bulk_actions_checkbox') !== -1) { + // Skip if col is null or doesn't have required properties + if (!col || col.columnIndex === undefined) { + return ''; + } + + if (col.data && typeof col.data === 'string' && col.data.indexOf('crud_bulk_actions_checkbox') !== -1) { col.data = col.data.replace('crud_bulk_actions_checkbox', 'crud_bulk_actions_checkbox d-none'); } @@ -371,8 +410,8 @@ functionsToRunOnDataTablesDrawEvent: [], } return ''+ - ' '+ - ''+ + ' '+ + ''+ ''; }).join(''); @@ -394,6 +433,10 @@ functionsToRunOnDataTablesDrawEvent: [], var tableId = settings.sTableId; var table = window.crud.tables[tableId]; + if (!table || typeof table.columns !== 'function') { + return; + } + data.columns.forEach(function(item, index) { var columnHeading = table.columns().header()[index]; if ($(columnHeading).attr('data-visible-in-table') == 'true') { @@ -444,6 +487,10 @@ functionsToRunOnDataTablesDrawEvent: [], d.totalEntryCount = tableElement.getAttribute('data-total-entry-count') || false; d.datatable_id = tableId; return d; + }, + "dataSrc": function(json) { + + return json.data; } }; } @@ -504,6 +551,11 @@ functionsToRunOnDataTablesDrawEvent: [], const tableId = $(this).attr('id'); if (!tableId) return; + // Skip tables inside modals + if ($(this).closest('.modal').length > 0) { + return; + } + if ($.fn.DataTable.isDataTable('#' + tableId)) { return; } @@ -613,19 +665,73 @@ function setupTableEvents(tableId, config) { // on DataTable draw event run all functions in the queue $(`#${tableId}`).on('draw.dt', function() { + + // Ensure initializeAllModals function is available before we try to call it + if (typeof window.initializeAllModals === 'undefined') { + window.initializeAllModals = function() { + // This is a basic fallback that will be replaced by the full implementation + // when the modal script loads + }; + } + + const modalTemplatesInTable = document.getElementById(tableId).querySelectorAll('[id^="modalTemplate"]'); + + modalTemplatesInTable.forEach(function(modal, index) { + const newModal = modal.cloneNode(true); + document.body.appendChild(newModal); + modal.remove(); + }); + + // After moving modals, check what's now in the DOM + const allModalTemplates = document.querySelectorAll('[id^="modalTemplate"]'); + + // After moving modals, trigger initialization if the function exists + if (typeof window.initializeAllModals === 'function') { + window.initializeAllModals(); + } else { + console.warn('window.initializeAllModals function not found'); + } // in datatables 2.0.3 the implementation was changed to use `replaceChildren`, for that reason scripts // that came with the response are no longer executed, like the delete button script or any other ajax // button created by the developer. For that reason, we move them to the end of the body // ensuring they are re-evaluated on each draw event. - document.getElementById(tableId).querySelectorAll('script').forEach(function(script) { - const scriptsToLoad = []; - if (script.src) { - // For external scripts with src attribute - const srcUrl = script.src; - - // Only load the script if it's not already loaded - if (!document.querySelector(`script[src="${srcUrl}"]`)) { - scriptsToLoad.push(new Promise((resolve, reject) => { + try { + const tableElement = document.getElementById(tableId); + if (tableElement) { + document.getElementById(tableId).querySelectorAll('script').forEach(function(script) { + const scriptsToLoad = []; + if (script.src) { + // For external scripts with src attribute + const srcUrl = script.src; + + // Only load the script if it's not already loaded + if (!document.querySelector(`script[src="${srcUrl}"]`)) { + scriptsToLoad.push(new Promise((resolve, reject) => { + const newScript = document.createElement('script'); + + // Copy all attributes from the original script + Array.from(script.attributes).forEach(attr => { + newScript.setAttribute(attr.name, attr.value); + }); + + // Set up load and error handlers + newScript.onload = resolve; + newScript.onerror = reject; + + // Append to document to start loading + try { + document.head.appendChild(newScript); + } catch (e) { + console.warn('Error appending external script:', e); + reject(e); + } + })); + } + + // Remove the original script tag + script.parentNode.removeChild(script); + } else { + // For inline scripts const newScript = document.createElement('script'); // Copy all attributes from the original script @@ -633,37 +739,23 @@ function setupTableEvents(tableId, config) { newScript.setAttribute(attr.name, attr.value); }); - // Set up load and error handlers - newScript.onload = resolve; - newScript.onerror = reject; - - // Append to document to start loading - document.head.appendChild(newScript); - })); - } - - // Remove the original script tag - script.parentNode.removeChild(script); - } else { - // For inline scripts - const newScript = document.createElement('script'); - - // Copy all attributes from the original script - Array.from(script.attributes).forEach(attr => { - newScript.setAttribute(attr.name, attr.value); - }); + // Copy the content + newScript.textContent = script.textContent; - // Copy the content - newScript.textContent = script.textContent; - - try { - document.head.appendChild(newScript); - }catch (e) { - console.warn('Error appending inline script:', e); - } - } - - }); + try { + document.head.appendChild(newScript); + }catch (e) { + console.warn('Error appending inline script:', e); + } + } + + }); + } else { + console.warn('Table element not found:', tableId); + } + } catch (e) { + console.warn('Error processing scripts for table:', tableId, e); + } // Run table-specific functions and pass the tableId // to the function diff --git a/src/resources/views/crud/fields/color.blade.php b/src/resources/views/crud/fields/color.blade.php index 0ff88efc5c..f47c72b4df 100644 --- a/src/resources/views/crud/fields/color.blade.php +++ b/src/resources/views/crud/fields/color.blade.php @@ -1,6 +1,11 @@ {{-- html5 color input --}} @php $value = old_empty_or_null($field['name'], '') ?? $field['value'] ?? $field['default'] ?? ''; +// HTML5 color input requires a valid hex color format (#rrggbb) +// If the value is empty or invalid, default to black (#000000) +if (empty($value) || !preg_match('/^#[0-9a-f]{6}$/i', $value)) { + $value = $field['default'] ?? '#000000'; +} @endphp @@ -59,6 +64,16 @@ function bpFieldInitColorElement(element) { let inputText = element[0]; let inputColor = inputText.nextElementSibling.querySelector('input'); + // Ensure color input has a valid value (HTML5 requires #rrggbb format) + if (!inputColor.value || !inputColor.value.match(/^#[0-9a-f]{6}$/i)) { + inputColor.value = '#000000'; + } + + // Ensure text input matches color input + if (!inputText.value || !inputText.value.match(/^#[0-9a-f]{6}$/i)) { + inputText.value = inputColor.value; + } + inputText.addEventListener('input', () => inputText.value = inputColor.value = '#' + inputText.value.replace(/[^\da-f]/gi, '').toLowerCase()); inputColor.addEventListener('input', () => inputText.value = inputColor.value); } diff --git a/src/resources/views/crud/fields/summernote.blade.php b/src/resources/views/crud/fields/summernote.blade.php index 4a8e08d4b8..8f53f8a468 100644 --- a/src/resources/views/crud/fields/summernote.blade.php +++ b/src/resources/views/crud/fields/summernote.blade.php @@ -41,6 +41,10 @@ .note-editor.note-frame .note-status-output, .note-editor.note-airframe .note-status-output { height: auto; } + + .note-modal { + z-index: 1060 !important; /* Higher than Bootstrap's default modal z-index */ + } @endBassetBlock @endpush diff --git a/src/resources/views/crud/form_content.blade.php b/src/resources/views/crud/form_content.blade.php index 54c1e81eb2..8a195ca49b 100644 --- a/src/resources/views/crud/form_content.blade.php +++ b/src/resources/views/crud/form_content.blade.php @@ -1,4 +1,4 @@ -route) }}> + {{-- See if we're using tabs --}} @@ -33,75 +33,13 @@ @stack('crud_fields_scripts')
'+colTitle+':'+''+col.data+''+colTitle+':'+''+(col.data || '')+'