diff --git a/src/CrudPanelManager.php b/src/CrudPanelManager.php index cb7af75a2f..e7cfe4add7 100644 --- a/src/CrudPanelManager.php +++ b/src/CrudPanelManager.php @@ -69,8 +69,9 @@ public function setupCrudPanel(string $controller, ?string $operation = null): C $crud->setOperation($operation); $primaryControllerRequest = $this->cruds[array_key_first($this->cruds)]->getRequest(); - if (! $crud->isInitialized()) { + if (! $crud->isInitialized() || ! $this->isOperationInitialized($controller::class, $operation)) { self::setActiveController($controller::class); + $crud->initialized = false; $controller->initializeCrudPanel($primaryControllerRequest, $crud); self::unsetActiveController(); $crud = $this->cruds[$controller::class]; @@ -106,6 +107,14 @@ public function getInitializedOperations(string $controller): array return $this->initializedOperations[$controller] ?? []; } + /** + * Check if a specific operation has been initialized for a controller. + */ + public function isOperationInitialized(string $controller, string $operation): bool + { + return in_array($operation, $this->getInitializedOperations($controller), true); + } + /** * Store a CrudPanel instance for a specific controller. */ diff --git a/src/app/Http/Controllers/Operations/CreateOperation.php b/src/app/Http/Controllers/Operations/CreateOperation.php index e6282ff68f..a6dc038747 100644 --- a/src/app/Http/Controllers/Operations/CreateOperation.php +++ b/src/app/Http/Controllers/Operations/CreateOperation.php @@ -60,7 +60,9 @@ public function create() $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 request()->ajax() ? + view('crud::components.form.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..e6eacf0a28 100644 --- a/src/app/Http/Controllers/Operations/UpdateOperation.php +++ b/src/app/Http/Controllers/Operations/UpdateOperation.php @@ -82,7 +82,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() ? + view('crud::components.form.ajax_response', $this->data) : + view($this->crud->getEditView(), $this->data); } /** diff --git a/src/app/Library/CrudPanel/CrudButton.php b/src/app/Library/CrudPanel/CrudButton.php index 5f2f711931..1fabb2ca8d 100644 --- a/src/app/Library/CrudPanel/CrudButton.php +++ b/src/app/Library/CrudPanel/CrudButton.php @@ -295,6 +295,7 @@ public function section($stack) * The HTML itself of the button. * * @param object|null $entry The eloquent Model for the current entry or null if no current entry. + * @param CrudPanel|null $crud The CrudPanel object, if not passed it will be retrieved from the service container. * @return \Illuminate\Contracts\View\View */ public function getHtml($entry = null, ?CrudPanel $crud = null) diff --git a/src/app/View/Components/Form.php b/src/app/View/Components/Form.php new file mode 100644 index 0000000000..a174955743 --- /dev/null +++ b/src/app/View/Components/Form.php @@ -0,0 +1,61 @@ +getOperation(); + } + + $this->crud = CrudManager::setupCrudPanel($controller, $operation); + + if (isset($previousOperation)) { + $this->crud->setOperation($previousOperation); + } + + $this->operation = $operation; + $this->action = $action ?? url($this->crud->route); + $this->hasUploadFields = $this->crud->hasUploadFields($operation); + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View|\Closure|string + */ + public function render() + { + return view('crud::components.form.form', [ + 'crud' => $this->crud, + 'saveAction' => $this->crud->getSaveAction(), + 'id' => $this->id, + 'operation' => $this->operation, + 'action' => $this->action, + 'method' => $this->method, + ]); + } +} diff --git a/src/app/View/Components/FormModal.php b/src/app/View/Components/FormModal.php new file mode 100644 index 0000000000..adb302ab4f --- /dev/null +++ b/src/app/View/Components/FormModal.php @@ -0,0 +1,53 @@ + $this->crud, + 'id' => $this->id, + 'operation' => $this->operation, + 'formRouteOperation' => $this->formRouteOperation, + 'hasUploadFields' => $this->hasUploadFields, + 'refreshDatatable' => $this->refreshDatatable, + 'action' => $this->action, + 'method' => $this->method, + 'title' => $this->title, + 'classes' => $this->classes, + ]); + } +} diff --git a/src/resources/assets/css/common.css b/src/resources/assets/css/common.css index 9c79cc1fc8..87b9222981 100644 --- a/src/resources/assets/css/common.css +++ b/src/resources/assets/css/common.css @@ -1,5 +1,6 @@ :root { --table-row-hover: #f2f1ff; + --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 { @@ -212,6 +213,10 @@ form .select2.select2-container { overflow: visible; } +.modal .btn-close, .modal .close { + background: transparent var(--btn-close-bg) center / .75rem auto no-repeat; +} + /* SELECT 2 */ .select2-container--bootstrap .select2-selection { box-shadow: none !important; diff --git a/src/resources/assets/js/form_modal.js b/src/resources/assets/js/form_modal.js new file mode 100644 index 0000000000..8d7123e408 --- /dev/null +++ b/src/resources/assets/js/form_modal.js @@ -0,0 +1,313 @@ +(function() { + // Initialize modals immediately when the script runs + initializeAllModals(); + // Also listen for DataTable draw events which might add new modals + document.addEventListener('draw.dt', initializeAllModals); +})(); + function initializeAllModals() { + // First, track all initialized modals by their unique ID to avoid duplicates + const initializedModals = new Set(); + + document.querySelectorAll('[id^="modalTemplate"]').forEach(modalTemplate => { + // Extract controller hash from the ID + const controllerId = modalTemplate.id.replace('modalTemplate', ''); + const modalEl = modalTemplate.querySelector('.modal'); + if(!modalEl) { + console.warn(`No modal found in template with ID ${modalTemplate.id}`); + return; + } + + const modalId = modalEl.id; + + // Create a unique key for this modal + const modalKey = `${controllerId}-${modalId}`; + + // Skip if we've already processed an identical modal in this batch + if (initializedModals.has(modalKey)) { + console.log('Skipping already processed modal:', modalTemplate.id); + modalTemplate.remove(); // Remove duplicate + return; + } + + initializedModals.add(modalKey); + + // Get other elements + const formContainer = document.getElementById(`modal-form-container${controllerId}`); + const submitButton = document.getElementById(`submitForm${controllerId}`); + if (!formContainer || !submitButton) { + console.warn(`Missing form elements for controller ID ${controllerId}`); + return; + } + + // Make modal template visible (but modal stays hidden until triggered) + modalTemplate.classList.remove('d-none'); + + // Only set up the event handlers if they don't exist yet + if (!modalEl._loadHandler) { + modalEl._loadHandler = function() { + loadModalForm(controllerId, modalEl, formContainer, submitButton); + }; + modalEl.addEventListener('shown.bs.modal', modalEl._loadHandler); + } + + if (!submitButton._saveHandler) { + submitButton._saveHandler = function() { + submitModalForm(controllerId, formContainer, submitButton, modalEl); + }; + submitButton.addEventListener('click', submitButton._saveHandler); + } + + // Mark as initialized + modalTemplate.setAttribute('data-initialized', 'true'); + + // Initialize Bootstrap modal if it hasn't been initialized yet + if (typeof bootstrap !== 'undefined' && !bootstrap.Modal.getInstance(modalEl)) { + new bootstrap.Modal(modalEl); + } + }); +} + + // Load form contents via AJAX + function loadModalForm(controllerId, modalEl, formContainer, submitButton) { + + submitButton.disabled = true; + + if (formContainer && !formContainer.dataset.loaded) { + // Build URL from current path + const formUrl = formContainer.dataset.formLoadRoute || modalEl.dataset.formLoadRoute || ''; + + fetch(formUrl, { + method: 'GET', + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(response => response.text()) + .then(html => { + if (!html) { + console.error(`No HTML content returned for controller ID ${controllerId}`); + return; + } + + // add the enctype to the form if it has upload fields + if (formContainer.dataset.hasUploadFields === 'true') { + html = html.replace(/
+ + +@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('after_styles') +@stack('after_scripts') \ No newline at end of file diff --git a/src/resources/views/crud/components/form/form.blade.php b/src/resources/views/crud/components/form/form.blade.php new file mode 100644 index 0000000000..1952fb4c6e --- /dev/null +++ b/src/resources/views/crud/components/form/form.blade.php @@ -0,0 +1,49 @@ +