Files
citrus/resources/views/components/placeholder-picker.blade.php
T

388 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@php
// Desteklenen placeholder tipleri ve açıklamaları
$placeholderTypes = [
'text' => ['label' => __('placeholder-picker.type_text'), 'examples' => ['company_name', 'title', 'description', 'subtitle']],
'email' => ['label' => __('placeholder-picker.type_email'), 'examples' => ['contact', 'support', 'info', 'sales']],
'url' => ['label' => __('placeholder-picker.type_url'), 'examples' => ['website', 'facebook', 'twitter', 'linkedin']],
'tel' => ['label' => __('placeholder-picker.type_tel'), 'examples' => ['phone', 'mobile', 'fax', 'hotline']],
'number' => ['label' => __('placeholder-picker.type_number'), 'examples' => ['price', 'quantity', 'age', 'year']],
'textarea' => ['label' => __('placeholder-picker.type_textarea'), 'examples' => ['content', 'about', 'description', 'notes']],
'richtext' => ['label' => __('placeholder-picker.type_richtext'), 'examples' => ['content', 'article', 'post', 'body']],
'markdown' => ['label' => __('placeholder-picker.type_markdown'), 'examples' => ['content', 'documentation', 'readme']],
'code' => ['label' => __('placeholder-picker.type_code'), 'examples' => ['snippet', 'example', 'script']],
'date' => ['label' => __('placeholder-picker.type_date'), 'examples' => ['publish_date', 'event_date', 'birthday']],
'datetime' => ['label' => __('placeholder-picker.type_datetime'), 'examples' => ['created_at', 'updated_at', 'event_time']],
'time' => ['label' => __('placeholder-picker.type_time'), 'examples' => ['start_time', 'end_time', 'opening_time']],
'image' => ['label' => __('placeholder-picker.type_image'), 'examples' => ['logo', 'banner', 'avatar', 'thumbnail']],
'images' => ['label' => __('placeholder-picker.type_images'), 'examples' => ['gallery', 'photos', 'slider_images']],
'file' => ['label' => __('placeholder-picker.type_file'), 'examples' => ['document', 'pdf', 'download']],
'files' => ['label' => __('placeholder-picker.type_files'), 'examples' => ['documents', 'attachments']],
'select' => ['label' => __('placeholder-picker.type_select'), 'examples' => ['category', 'status', 'type']],
'multiselect' => ['label' => __('placeholder-picker.type_multiselect'), 'examples' => ['tags', 'categories', 'features']],
'checkbox' => ['label' => __('placeholder-picker.type_checkbox'), 'examples' => ['is_active', 'is_featured', 'is_published']],
'radio' => ['label' => __('placeholder-picker.type_radio'), 'examples' => ['gender', 'type', 'status']],
'toggle' => ['label' => __('placeholder-picker.type_toggle'), 'examples' => ['is_active', 'is_enabled', 'is_visible']],
'color' => ['label' => __('placeholder-picker.type_color'), 'examples' => ['primary_color', 'background_color', 'text_color']],
'tags' => ['label' => __('placeholder-picker.type_tags'), 'examples' => ['tags', 'keywords', 'labels']],
];
// Field name'i component'ten al
$fieldName = $fieldName ?? 'html_content';
@endphp
<div
x-data="placeholderPickerData(@js($placeholderTypes))"
>
<x-filament::section
collapsible
:collapsed="true"
:description="__('placeholder-picker.click_to_insert')"
icon="heroicon-o-tag"
>
<x-slot name="heading">
{{ __('placeholder-picker.title') }}
</x-slot>
<div class="space-y-4">
<!-- Search Bar -->
<x-filament::input.wrapper>
<x-slot name="prefix">
<x-filament::icon
icon="heroicon-o-magnifying-glass"
class="h-5 w-5"
/>
</x-slot>
<x-filament::input
type="text"
x-model="searchQuery"
:placeholder="__('placeholder-picker.search_placeholder')"
/>
<x-slot name="suffix">
<button
x-show="searchQuery"
@click="searchQuery = ''"
type="button"
class="fi-icon-btn fi-icon-btn-color-gray fi-icon-btn-size-md"
>
<x-filament::icon
icon="heroicon-o-x-mark"
class="h-4 w-4"
/>
</button>
</x-slot>
</x-filament::input.wrapper>
<!-- Placeholder List -->
<div class="max-h-96 overflow-y-auto space-y-4">
<template x-for="(info, type) in filteredCategories" x-bind:key="type">
<div class="space-y-2">
<h4 class="fi-section-header-heading text-xs font-semibold uppercase tracking-wide">
<span x-text="info.label"></span>
</h4>
<div class="flex flex-wrap gap-2">
<template x-for="example in info.examples" x-bind:key="example">
<x-filament::button
size="sm"
color="gray"
outlined
@click="insertPlaceholder('{{ $fieldName }}', type + '.' + example)"
x-bind:tooltip="'{{ __('placeholder-picker.click_to_insert') }}'"
>
<code class="text-xs font-mono" x-text="'{' + type + '.' + example + '}'"></code>
</x-filament::button>
</template>
</div>
</div>
</template>
<!-- Özel placeholder'lar -->
<div x-show="hasSpecialPlaceholders" class="pt-3 border-t fi-border-color">
<h4 class="fi-section-header-heading text-xs font-semibold uppercase tracking-wide mb-2">
{{ __('placeholder-picker.special_placeholders') }}
</h4>
<div class="flex flex-wrap gap-2">
<x-filament::button
size="sm"
color="primary"
outlined
@click="insertPlaceholder('{{ $fieldName }}', 'menu')"
>
<code class="text-xs font-mono">{menu}</code>
</x-filament::button>
<x-filament::button
size="sm"
color="primary"
outlined
@click="insertPlaceholder('{{ $fieldName }}', 'staticMenu')"
>
<code class="text-xs font-mono">{staticMenu}</code>
</x-filament::button>
</div>
</div>
<!-- No Results -->
<div
x-show="Object.keys(filteredCategories).length === 0 && !hasSpecialPlaceholders"
class="flex flex-col items-center justify-center gap-4 py-8"
>
<x-filament::icon
icon="heroicon-o-magnifying-glass"
class="h-12 w-12 text-gray-400 dark:text-gray-500"
/>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ __('placeholder-picker.no_results') }}</p>
</div>
</div>
</div>
</x-filament::section>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('placeholderPickerData', (placeholderTypes) => ({
searchQuery: '',
placeholderTypes: placeholderTypes,
get filteredCategories() {
if (!this.searchQuery.trim()) {
return this.placeholderTypes;
}
const query = this.searchQuery.toLowerCase();
const filtered = {};
for (const [type, info] of Object.entries(this.placeholderTypes)) {
const filteredExamples = info.examples.filter(example => {
const placeholder = type + '.' + example;
return placeholder.toLowerCase().includes(query) ||
example.toLowerCase().includes(query) ||
info.label.toLowerCase().includes(query);
});
if (filteredExamples.length > 0) {
filtered[type] = {
label: info.label,
examples: filteredExamples
};
}
}
return filtered;
},
get hasSpecialPlaceholders() {
if (!this.searchQuery.trim()) return true;
const query = this.searchQuery.toLowerCase();
return 'menu'.includes(query) || 'staticmenu'.includes(query) || 'özel'.includes(query) || 'special'.includes(query);
}
}));
});
function insertPlaceholder(fieldName, placeholder) {
const placeholderText = '{' + placeholder + '}';
// Monaco Editor instance'ını bul
const editorContainer = document.querySelector('[data-field-name=\'' + fieldName + '\']');
let monacoEditorInstance = null;
let currentValue = '';
let cursorPosition = null;
// Monaco Editor'dan cursor pozisyonunu al
if (editorContainer && window.monaco) {
const editorInstances = window.monaco.editor.getEditors();
for (let editor of editorInstances) {
const container = editor.getContainerDomNode();
if (container && container.closest && container.closest('[data-field-name=\'' + fieldName + '\']')) {
monacoEditorInstance = editor;
const model = editor.getModel();
currentValue = model.getValue();
const selection = editor.getSelection();
if (selection) {
cursorPosition = model.getOffsetAt(selection.getStartPosition());
} else {
cursorPosition = currentValue.length;
}
break;
}
}
}
// Monaco Editor varsa, doğrudan onu kullan
if (monacoEditorInstance) {
const model = monacoEditorInstance.getModel();
const newValue = currentValue.slice(0, cursorPosition) + placeholderText + currentValue.slice(cursorPosition);
// Monaco Editor'da değeri güncelle
monacoEditorInstance.executeEdits('placeholder-insert', [{
range: new window.monaco.Range(
model.getPositionAt(cursorPosition).lineNumber,
model.getPositionAt(cursorPosition).column,
model.getPositionAt(cursorPosition).lineNumber,
model.getPositionAt(cursorPosition).column
),
text: placeholderText
}]);
// Cursor pozisyonunu güncelle
const newPosition = model.getPositionAt(cursorPosition + placeholderText.length);
monacoEditorInstance.setPosition(newPosition);
monacoEditorInstance.focus();
// Filament form state'ini güncellemek için Monaco Editor'ın change event'ini tetikle
// Filament CodeEditor otomatik olarak değişiklikleri algılar
setTimeout(() => {
// Monaco Editor model değişikliğini tetikle
// Bu, Filament'in CodeEditor component'inin otomatik olarak algılamasını sağlar
if (monacoEditorInstance) {
// Model'in değerini tekrar set et (zaten set ettik ama event'i tetiklemek için)
const model = monacoEditorInstance.getModel();
// Monaco Editor'ın onDidChangeContent event'ini manuel tetikle
// Filament CodeEditor bu event'i dinliyor ve form state'ini güncelliyor
model.onDidChangeContent(() => {
// Bu event zaten tetiklenmiş olacak, sadece emin olmak için
});
// Hidden input'u bul ve güncelle (eğer varsa)
const hiddenInput = editorContainer.querySelector('input[type=\'hidden\']');
if (hiddenInput) {
hiddenInput.value = newValue;
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
}
// Filament form'unu bul ve güncelle
// Form component'ini bulmak için editor container'ın parent'larını kontrol et
let formComponent = null;
const formWireElement = editorContainer.closest('form [wire\\:id], [wire\\:id][wire\\:key*="form"]');
if (formWireElement && window.Livewire) {
const wireId = formWireElement.getAttribute('wire:id');
const component = window.Livewire.find(wireId);
// Component'in form component'i olup olmadığını kontrol et
// Sadece field path'lerini kontrol et, 'data' property'sine direkt erişme
if (component && component.get) {
const fieldPath = 'data.' + fieldName;
try {
// Field path'i kontrol et
if (component.get(fieldPath) !== undefined) {
formComponent = component;
} else if (component.get('data.html_content') !== undefined) {
formComponent = component;
} else if (component.get('html_content') !== undefined) {
formComponent = component;
}
} catch (e) {
// Property kontrolü sırasında hata alırsak, component'i kullanma
}
}
}
// Sadece form component'i bulunduysa güncelle
if (formComponent) {
try {
const fieldPath = 'data.' + fieldName;
if (formComponent.get(fieldPath) !== undefined) {
formComponent.set(fieldPath, newValue);
} else if (formComponent.get('data.html_content') !== undefined) {
formComponent.set('data.html_content', newValue);
} else if (formComponent.get('html_content') !== undefined) {
formComponent.set('html_content', newValue);
}
} catch (e) {
// Hata durumunda sadece console'a yaz, Monaco Editor zaten güncellenmiş
console.debug('Livewire form state update skipped:', e.message);
}
}
}
}, 150);
return;
}
// Livewire component'ini bul ve güncelle (Monaco Editor yoksa)
if (window.Livewire) {
// Form component'ini bulmak için field name ile input elementini bul
const formField = document.querySelector('input[name*=\'' + fieldName + '\'], textarea[name*=\'' + fieldName + '\']');
let formComponent = null;
if (formField) {
// Input field'ın parent'ındaki wire element'i bul
const formWireElement = formField.closest('[wire\\:id]');
if (formWireElement) {
const wireId = formWireElement.getAttribute('wire:id');
const component = window.Livewire.find(wireId);
// Component'in form component'i olup olmadığını kontrol et
if (component && component.get) {
const fieldPath = 'data.' + fieldName;
try {
// Field path'lerini kontrol et
if (component.get(fieldPath) !== undefined ||
component.get('data.html_content') !== undefined ||
component.get('html_content') !== undefined) {
formComponent = component;
}
} catch (e) {
// Property kontrolü sırasında hata alırsak, component'i kullanma
}
}
}
}
if (formComponent) {
try {
// Mevcut değeri al
let fieldValue = '';
if (formComponent.get('data.html_content') !== undefined) {
fieldValue = formComponent.get('data.html_content') || '';
} else if (formComponent.get('html_content') !== undefined) {
fieldValue = formComponent.get('html_content') || '';
} else {
const fieldPath = 'data.' + fieldName;
fieldValue = formComponent.get(fieldPath) || '';
}
// Cursor pozisyonu yoksa sona ekle
if (cursorPosition === null) {
cursorPosition = fieldValue.length;
}
// Placeholder'ı cursor pozisyonuna ekle
const newValue = fieldValue.slice(0, cursorPosition) + placeholderText + fieldValue.slice(cursorPosition);
// Livewire state'ini güncelle (Livewire 3 API)
if (formComponent.get('data.html_content') !== undefined) {
formComponent.set('data.html_content', newValue);
} else if (formComponent.get('html_content') !== undefined) {
formComponent.set('html_content', newValue);
} else {
const fieldPath = 'data.' + fieldName;
formComponent.set(fieldPath, newValue);
}
return;
} catch (e) {
console.warn('Livewire form update failed:', e);
}
}
}
// Fallback: Textarea veya input elementini bul
const formField = document.querySelector('input[name*=\'' + fieldName + '\'], textarea[name*=\'' + fieldName + '\']');
if (formField) {
const fieldValue = formField.value || '';
const fieldCursorPosition = formField.selectionStart || fieldValue.length;
const newValue = fieldValue.slice(0, fieldCursorPosition) + placeholderText + fieldValue.slice(fieldCursorPosition);
formField.value = newValue;
formField.setSelectionRange(fieldCursorPosition + placeholderText.length, fieldCursorPosition + placeholderText.length);
// Form event'lerini tetikle
formField.dispatchEvent(new Event('input', { bubbles: true }));
formField.dispatchEvent(new Event('change', { bubbles: true }));
}
}
// Global scope'a ekle
window.insertPlaceholder = insertPlaceholder;
</script>