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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user