Implement fullscreen editor for template preview: Added a fullscreen editing feature with collapsible editor functionality and improved loading indicators. Enhanced the template preview Blade view for better user interaction and experience during content editing and previewing.

This commit is contained in:
Ümit Tunç
2025-10-31 17:14:21 -03:00
parent 6f12dda0d9
commit 8d7b3f0914
2 changed files with 353 additions and 5 deletions
@@ -8,6 +8,8 @@ use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\View;
use Filament\Schemas\Schema;
use Filament\Forms\Components\CodeEditor\Enums\Language;
class FooterTemplateForm
{
@@ -26,6 +28,7 @@ class FooterTemplateForm
CodeEditor::make('html_content')
->label(__('footer-templates.field_html_content'))
->required()
->language(Language::Html)
->live(onBlur: false) // Real-time updates
->columnSpanFull()
->helperText(__('footer-templates.field_html_content_help'))
@@ -16,19 +16,28 @@
</div>
<div class="mt-2 flex items-center justify-end gap-2 mb-2">
<button
@click="openFullscreenEditor()"
class="fi-btn fi-btn-color-gray fi-btn-size-sm inline-flex items-center justify-center rounded-lg font-semibold outline-none transition duration-75 focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-70 bg-gray-600 text-white hover:bg-gray-500 dark:bg-gray-500 dark:hover:bg-gray-400 p-2"
type="button"
title="Tam Ekran Editörü Aç">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
</svg>
</button>
<button
@click="updatePreview()"
:disabled="isLoading"
class="fi-btn fi-btn-color-primary fi-btn-size-sm inline-flex items-center gap-2 justify-center rounded-lg font-semibold outline-none transition duration-75 focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-70 bg-primary-600 text-white hover:bg-primary-500 dark:bg-primary-500 dark:hover:bg-primary-400 px-3 py-2"
type="button">
<svg x-show="!isLoading" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
class="fi-btn fi-btn-color-primary fi-btn-size-sm inline-flex items-center justify-center rounded-lg font-semibold outline-none transition duration-75 focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-70 bg-primary-600 text-white hover:bg-primary-500 dark:bg-primary-500 dark:hover:bg-primary-400 p-2"
type="button"
title="Önizlemeyi Çalıştır">
<svg x-show="!isLoading" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<svg x-show="isLoading" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg x-show="isLoading" class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span x-text="isLoading ? 'Yükleniyor...' : 'Run'"></span>
</button>
</div>
@@ -68,6 +77,106 @@
</iframe>
</div>
<!-- Fullscreen Editor Modal (CodePen Style) -->
<div
x-show="isFullscreen"
x-cloak
class="fixed inset-0 z-[9999] bg-gray-900 dark:bg-gray-950"
style="display: none;"
x-transition
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<!-- Top Toolbar -->
<div class="absolute top-0 left-0 right-0 z-10 bg-gray-800 dark:bg-gray-900 border-b border-gray-700 dark:border-gray-800 px-4 py-2 flex items-center justify-between">
<div class="flex items-center gap-2">
<h3 class="text-white font-semibold text-sm">HTML Editör & Önizleme</h3>
</div>
<div class="flex items-center gap-2">
<button
@click="toggleEditorCollapse()"
class="px-3 py-1.5 text-sm text-gray-300 hover:text-white hover:bg-gray-700 dark:hover:bg-gray-800 rounded transition"
type="button">
<svg x-show="!editorCollapsed" class="w-4 h-4 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
</svg>
<svg x-show="editorCollapsed" class="w-4 h-4 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
<span class="ml-1" x-text="editorCollapsed ? 'Editörü Göster' : 'Editörü Gizle'"></span>
</button>
<button
@click="updatePreview()"
:disabled="isLoading"
class="p-2 bg-primary-600 hover:bg-primary-500 text-white rounded transition disabled:opacity-50"
type="button"
title="Önizlemeyi Çalıştır">
<svg x-show="!isLoading" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<svg x-show="isLoading" class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</button>
<button
@click="closeFullscreenEditor()"
class="px-3 py-1.5 text-sm text-gray-300 hover:text-white hover:bg-gray-700 dark:hover:bg-gray-800 rounded transition"
type="button">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<!-- Editor Section (Collapsible) -->
<div
class="absolute left-0 right-0 transition-all duration-300 ease-in-out overflow-hidden"
:class="editorCollapsed ? 'top-0 h-0' : 'top-10 h-[40vh]'"
style="background: #1e1e1e;">
<div
class="h-full w-full"
id="fullscreen-code-editor"
style="min-height: 100%;">
<!-- CodeMirror editor will be cloned here -->
</div>
</div>
<!-- Preview Section (Takes remaining space) -->
<div
class="absolute left-0 right-0 transition-all duration-300 ease-in-out"
:class="editorCollapsed ? 'top-0 h-full' : 'top-[calc(40vh+2.5rem)] h-[calc(100vh-40vh-2.5rem)]'"
style="background: white;">
<!-- Loading indicator -->
<div
x-show="isLoading || !fullscreenIframeSrc"
class="absolute inset-0 flex items-center justify-center bg-gray-100 dark:bg-gray-900"
style="z-index: 5;"
x-transition>
<div class="text-center px-4">
<div class="relative inline-block mb-4" x-show="isLoading">
<div class="w-16 h-16 border-4 border-gray-200 dark:border-gray-700 rounded-full animate-pulse"></div>
<div class="absolute top-0 left-0 w-16 h-16 border-4 border-transparent border-t-primary-600 dark:border-t-primary-400 rounded-full animate-spin"></div>
</div>
</div>
</div>
<!-- Preview iframe -->
<iframe
x-show="!isLoading && fullscreenIframeSrc"
:src="fullscreenIframeSrc"
class="w-full h-full border-0"
frameborder="0"
x-transition>
</iframe>
</div>
</div>
</div>
@@ -79,9 +188,14 @@ function templatePreview(previewUrl, type, fieldName) {
type: type,
fieldName: fieldName,
iframeSrc: '',
fullscreenIframeSrc: '',
isLoading: false,
content: '',
codeMirrorView: null,
isFullscreen: false,
editorCollapsed: false,
clonedEditor: null,
originalEditorWrapper: null,
init() {
console.log('[Preview] Init:', { previewUrl, type, fieldName });
@@ -92,6 +206,13 @@ function templatePreview(previewUrl, type, fieldName) {
this.findCodeMirror();
}, 1500);
});
// ESC key to close fullscreen
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isFullscreen) {
this.closeFullscreenEditor();
}
});
},
findCodeMirror() {
@@ -312,6 +433,10 @@ function templatePreview(previewUrl, type, fieldName) {
URL.revokeObjectURL(this.iframeSrc);
this.iframeSrc = '';
}
if (this.fullscreenIframeSrc) {
URL.revokeObjectURL(this.fullscreenIframeSrc);
this.fullscreenIframeSrc = '';
}
return;
}
@@ -338,12 +463,17 @@ function templatePreview(previewUrl, type, fieldName) {
const html = await response.text();
// Update both iframes
if (this.iframeSrc) {
URL.revokeObjectURL(this.iframeSrc);
}
if (this.fullscreenIframeSrc) {
URL.revokeObjectURL(this.fullscreenIframeSrc);
}
const blob = new Blob([html], { type: 'text/html; charset=utf-8' });
this.iframeSrc = URL.createObjectURL(blob);
this.fullscreenIframeSrc = URL.createObjectURL(blob);
this.isLoading = false;
console.log('[Preview] Updated successfully, content length:', this.content.length);
@@ -353,12 +483,227 @@ function templatePreview(previewUrl, type, fieldName) {
}
},
openFullscreenEditor() {
this.isFullscreen = true;
this.editorCollapsed = false;
this.$nextTick(() => {
setTimeout(() => {
this.cloneEditorToFullscreen();
}, 100);
});
},
closeFullscreenEditor() {
// Restore original editor if it was moved
if (this.clonedEditor && this.originalEditorWrapper) {
const editorContainer = this.clonedEditor.parentElement;
if (editorContainer && editorContainer.id === 'fullscreen-code-editor') {
// Move editor back to original location
const cmEditor = this.clonedEditor.querySelector('.cm-editor');
if (cmEditor && this.originalEditorWrapper) {
// Restore to original wrapper
const originalContainer = this.originalEditorWrapper.querySelector('.cm-editor').parentElement;
if (originalContainer) {
// Keep the editor where it is, just clean up
this.clonedEditor = null;
this.originalEditorWrapper = null;
}
}
}
}
this.isFullscreen = false;
this.editorCollapsed = false;
// Clean up fullscreen iframe
if (this.fullscreenIframeSrc) {
URL.revokeObjectURL(this.fullscreenIframeSrc);
this.fullscreenIframeSrc = '';
}
},
toggleEditorCollapse() {
this.editorCollapsed = !this.editorCollapsed;
},
cloneEditorToFullscreen() {
const fullscreenContainer = document.getElementById('fullscreen-code-editor');
if (!fullscreenContainer) {
console.error('[Preview] Fullscreen container not found');
return;
}
// Find the original CodeMirror editor
const wrapperSelectors = ['.fi-fo-field-wrp', '.fi-input-wrp', '[wire\\:id]'];
let targetWrapper = null;
const dataFieldElements = document.querySelectorAll(`[data-field-name="${this.fieldName}"]`);
if (dataFieldElements.length > 0) {
for (const selector of wrapperSelectors) {
targetWrapper = dataFieldElements[0].closest(selector);
if (targetWrapper) break;
}
}
if (!targetWrapper) {
const input = document.querySelector(`textarea[name="${this.fieldName}"], input[name="${this.fieldName}"]`);
if (input) {
for (const selector of wrapperSelectors) {
targetWrapper = input.closest(selector);
if (targetWrapper) break;
}
}
}
if (!targetWrapper) {
const cmEditor = document.querySelector('.cm-editor');
if (cmEditor) {
for (const selector of wrapperSelectors) {
targetWrapper = cmEditor.closest(selector);
if (targetWrapper) break;
}
}
}
if (!targetWrapper) {
console.error('[Preview] Could not find editor wrapper');
return;
}
this.originalEditorWrapper = targetWrapper;
// Find the CodeMirror editor element
const cmEditor = targetWrapper.querySelector('.cm-editor');
if (!cmEditor) {
console.error('[Preview] CodeMirror editor not found');
return;
}
// Clone the entire editor structure
const editorParent = cmEditor.parentElement;
if (!editorParent) {
console.error('[Preview] Editor parent not found');
return;
}
// Create a deep clone
const clone = editorParent.cloneNode(true);
clone.id = 'cloned-editor';
// Clear the container first
fullscreenContainer.innerHTML = '';
// Append clone
fullscreenContainer.appendChild(clone);
this.clonedEditor = clone;
// Adjust styles for fullscreen
const clonedCmEditor = clone.querySelector('.cm-editor');
if (clonedCmEditor) {
clonedCmEditor.style.height = '100%';
clonedCmEditor.style.minHeight = '100%';
// Make the cm-scroller take full height
const cmScroller = clonedCmEditor.querySelector('.cm-scroller');
if (cmScroller) {
cmScroller.style.height = '100%';
cmScroller.style.minHeight = '100%';
}
// Make the cm-content take full height
const cmContent = clonedCmEditor.querySelector('.cm-content');
if (cmContent) {
cmContent.style.minHeight = '100%';
}
}
// Update content watcher for cloned editor
const clonedCmContent = clone.querySelector('.cm-content');
if (clonedCmContent) {
// Watch for changes in cloned editor
const observer = new MutationObserver(() => {
const newContent = clonedCmContent.textContent || '';
if (newContent !== this.content) {
this.content = newContent;
// Also update original editor if it exists
const originalCmContent = cmEditor.querySelector('.cm-content');
if (originalCmContent) {
// Sync back to original if needed
}
}
});
observer.observe(clonedCmContent, {
childList: true,
subtree: true,
characterData: true,
});
// Update content from cloned editor
this.content = clonedCmContent.textContent || '';
}
// Try to sync CodeMirror 6 view if available
if (clonedCmEditor) {
if (clonedCmEditor.view || clonedCmEditor._view) {
const view = clonedCmEditor.view || clonedCmEditor._view;
if (view.state && view.state.doc) {
// Watch for changes
if (view.dispatch) {
const originalDispatch = view.dispatch;
view.dispatch = (tr) => {
originalDispatch.call(view, tr);
this.content = view.state.doc.toString();
};
}
}
}
}
console.log('[Preview] Editor cloned to fullscreen');
},
destroy() {
if (this.iframeSrc) {
URL.revokeObjectURL(this.iframeSrc);
}
if (this.fullscreenIframeSrc) {
URL.revokeObjectURL(this.fullscreenIframeSrc);
}
if (this.isFullscreen) {
this.closeFullscreenEditor();
}
}
};
}
</script>
@endpush
@push('styles')
<style>
[x-cloak] {
display: none !important;
}
/* Fullscreen editor styles */
#fullscreen-code-editor {
font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
}
#fullscreen-code-editor .cm-editor {
font-size: 14px;
line-height: 1.6;
}
#fullscreen-code-editor .cm-content {
padding: 1rem;
}
/* Smooth transitions */
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
</style>
@endpush