mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
feat: add codemirror
This commit is contained in:
parent
4c65c015b9
commit
b18d561361
8 changed files with 958 additions and 26 deletions
|
|
@ -0,0 +1,3 @@
|
|||
import { codeEditorFormComponent } from './codemirror-alpine.js';
|
||||
|
||||
window.codeEditorFormComponent = codeEditorFormComponent;
|
||||
198
resources/js/codemirror-alpine.js
Normal file
198
resources/js/codemirror-alpine.js
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import { createCodeMirror, getSystemTheme, watchThemeChange } from './codemirror-core.js';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
/**
|
||||
* Alpine.js component for CodeMirror that integrates with textarea and Livewire
|
||||
* Inspired by Filament's approach with proper state entanglement
|
||||
* @param {Object} config - Configuration object
|
||||
* @returns {Object} Alpine.js component object
|
||||
*/
|
||||
export function codeEditorFormComponent(config) {
|
||||
return {
|
||||
editor: null,
|
||||
textarea: null,
|
||||
isLoading: false,
|
||||
unwatchTheme: null,
|
||||
|
||||
// Configuration
|
||||
isDisabled: config.isDisabled || false,
|
||||
language: config.language || 'html',
|
||||
state: config.state || '',
|
||||
textareaId: config.textareaId || null,
|
||||
|
||||
/**
|
||||
* Initialize the component
|
||||
*/
|
||||
async init() {
|
||||
this.isLoading = true;
|
||||
|
||||
try {
|
||||
// Wait for textarea if provided
|
||||
if (this.textareaId) {
|
||||
await this.waitForTextarea();
|
||||
}
|
||||
|
||||
await this.$nextTick();
|
||||
this.createEditor();
|
||||
this.setupEventListeners();
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Wait for textarea to be available in the DOM
|
||||
*/
|
||||
async waitForTextarea() {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50; // 5 seconds max wait
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
this.textarea = document.getElementById(this.textareaId);
|
||||
if (this.textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait 100ms before trying again
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
console.error(`Textarea with ID "${this.textareaId}" not found after ${maxAttempts} attempts`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update both Livewire state and textarea with new value
|
||||
*/
|
||||
updateState(value) {
|
||||
this.state = value;
|
||||
if (this.textarea) {
|
||||
this.textarea.value = value;
|
||||
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the CodeMirror editor instance
|
||||
*/
|
||||
createEditor() {
|
||||
// Clean up any existing editor first
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
}
|
||||
|
||||
const effectiveTheme = this.getEffectiveTheme();
|
||||
const initialValue = this.textarea ? this.textarea.value : this.state;
|
||||
|
||||
this.editor = createCodeMirror(this.$refs.editor, {
|
||||
value: initialValue || '',
|
||||
language: this.language,
|
||||
theme: effectiveTheme,
|
||||
readOnly: this.isDisabled,
|
||||
onChange: (value) => this.updateState(value),
|
||||
onUpdate: (value) => this.updateState(value),
|
||||
onBlur: () => {
|
||||
if (this.editor) {
|
||||
this.updateState(this.editor.state.doc.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get effective theme
|
||||
*/
|
||||
getEffectiveTheme() {
|
||||
return getSystemTheme();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update editor content with new value
|
||||
*/
|
||||
updateEditorContent(value) {
|
||||
if (this.editor && value !== this.editor.state.doc.toString()) {
|
||||
this.editor.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: this.editor.state.doc.length,
|
||||
insert: value
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup event listeners for theme changes and state synchronization
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Watch for state changes from Livewire
|
||||
this.$watch('state', (newValue) => {
|
||||
this.updateEditorContent(newValue);
|
||||
});
|
||||
|
||||
// Watch for disabled state changes
|
||||
this.$watch('isDisabled', (newValue) => {
|
||||
if (this.editor) {
|
||||
this.editor.dispatch({
|
||||
effects: EditorView.editable.reconfigure(!newValue)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for textarea changes (from Livewire updates)
|
||||
if (this.textarea) {
|
||||
this.textarea.addEventListener('input', (event) => {
|
||||
this.updateEditorContent(event.target.value);
|
||||
this.state = event.target.value;
|
||||
});
|
||||
|
||||
// Listen for Livewire updates that might change the textarea value
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'value') {
|
||||
this.updateEditorContent(this.textarea.value);
|
||||
this.state = this.textarea.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(this.textarea, {
|
||||
attributes: true,
|
||||
attributeFilter: ['value']
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for theme changes
|
||||
this.unwatchTheme = watchThemeChange(() => {
|
||||
this.recreateEditor();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Recreate the editor (useful for theme changes)
|
||||
*/
|
||||
async recreateEditor() {
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
this.editor = null;
|
||||
await this.$nextTick();
|
||||
this.createEditor();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Clean up resources when component is destroyed
|
||||
*/
|
||||
destroy() {
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
this.editor = null;
|
||||
}
|
||||
if (this.unwatchTheme) {
|
||||
this.unwatchTheme();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
255
resources/js/codemirror-core.js
Normal file
255
resources/js/codemirror-core.js
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
import { EditorView, lineNumbers, keymap } from '@codemirror/view';
|
||||
import { ViewPlugin } from '@codemirror/view';
|
||||
import { indentWithTab } from '@codemirror/commands';
|
||||
import { foldGutter, foldKeymap } from '@codemirror/language';
|
||||
import { history, historyKeymap } from '@codemirror/commands';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { liquid } from '@codemirror/lang-liquid';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { githubLight } from '@fsegurai/codemirror-theme-github-light';
|
||||
|
||||
// Language support mapping
|
||||
const LANGUAGE_MAP = {
|
||||
'javascript': javascript,
|
||||
'js': javascript,
|
||||
'json': json,
|
||||
'css': css,
|
||||
'liquid': liquid,
|
||||
'html': html,
|
||||
};
|
||||
|
||||
// Theme support mapping
|
||||
const THEME_MAP = {
|
||||
'light': githubLight,
|
||||
'dark': oneDark,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get language support based on language parameter
|
||||
* @param {string} language - Language name or comma-separated list
|
||||
* @returns {Array|Extension} Language extension(s)
|
||||
*/
|
||||
function getLanguageSupport(language) {
|
||||
// Handle comma-separated languages
|
||||
if (language.includes(',')) {
|
||||
const languages = language.split(',').map(lang => lang.trim().toLowerCase());
|
||||
const languageExtensions = [];
|
||||
|
||||
languages.forEach(lang => {
|
||||
const languageFn = LANGUAGE_MAP[lang];
|
||||
if (languageFn) {
|
||||
languageExtensions.push(languageFn());
|
||||
}
|
||||
});
|
||||
|
||||
return languageExtensions;
|
||||
}
|
||||
|
||||
// Handle single language
|
||||
const languageFn = LANGUAGE_MAP[language.toLowerCase()] || LANGUAGE_MAP.html;
|
||||
return languageFn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme support
|
||||
* @param {string} theme - Theme name
|
||||
* @returns {Array} Theme extensions
|
||||
*/
|
||||
function getThemeSupport(theme) {
|
||||
const themeFn = THEME_MAP[theme] || THEME_MAP.light;
|
||||
return [themeFn];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resize plugin that handles container resizing
|
||||
* @returns {ViewPlugin} Resize plugin
|
||||
*/
|
||||
function createResizePlugin() {
|
||||
return ViewPlugin.fromClass(class {
|
||||
constructor(view) {
|
||||
this.view = view;
|
||||
this.resizeObserver = null;
|
||||
this.setupResizeObserver();
|
||||
}
|
||||
|
||||
setupResizeObserver() {
|
||||
const container = this.view.dom.parentElement;
|
||||
if (container) {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
// Use requestAnimationFrame to ensure proper timing
|
||||
requestAnimationFrame(() => {
|
||||
this.view.requestMeasure();
|
||||
});
|
||||
});
|
||||
this.resizeObserver.observe(container);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Flux-like theme styling based on theme
|
||||
* @param {string} theme - Theme name ('light', 'dark', or 'auto')
|
||||
* @returns {Object} Theme-specific styling
|
||||
*/
|
||||
function getFluxThemeStyling(theme) {
|
||||
const isDark = theme === 'dark' || (theme === 'auto' && getSystemTheme() === 'dark');
|
||||
|
||||
if (isDark) {
|
||||
return {
|
||||
backgroundColor: 'oklab(0.999994 0.0000455678 0.0000200868 / 0.1)',
|
||||
gutterBackgroundColor: 'oklch(26.9% 0 0)',
|
||||
borderColor: '#374151',
|
||||
focusBorderColor: 'rgb(224 91 68)',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
backgroundColor: '#fff', // zinc-50
|
||||
gutterBackgroundColor: '#fafafa', // zinc-50
|
||||
borderColor: '#e5e7eb', // gray-200
|
||||
focusBorderColor: 'rgb(224 91 68)', // red-500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CodeMirror editor instance
|
||||
* @param {HTMLElement} element - DOM element to mount editor
|
||||
* @param {Object} options - Editor options
|
||||
* @returns {EditorView} CodeMirror editor instance
|
||||
*/
|
||||
export function createCodeMirror(element, options = {}) {
|
||||
const {
|
||||
value = '',
|
||||
language = 'html',
|
||||
theme = 'light',
|
||||
readOnly = false,
|
||||
onChange = () => {},
|
||||
onUpdate = () => {},
|
||||
onBlur = () => {}
|
||||
} = options;
|
||||
|
||||
// Get language and theme support
|
||||
const languageSupport = getLanguageSupport(language);
|
||||
const themeSupport = getThemeSupport(theme);
|
||||
const fluxStyling = getFluxThemeStyling(theme);
|
||||
|
||||
// Create editor
|
||||
const editor = new EditorView({
|
||||
doc: value,
|
||||
extensions: [
|
||||
lineNumbers(),
|
||||
foldGutter(),
|
||||
history(),
|
||||
EditorView.lineWrapping,
|
||||
createResizePlugin(),
|
||||
...(Array.isArray(languageSupport) ? languageSupport : [languageSupport]),
|
||||
...themeSupport,
|
||||
keymap.of([indentWithTab, ...foldKeymap, ...historyKeymap]),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
fontSize: '14px',
|
||||
border: `1px solid ${fluxStyling.borderColor}`,
|
||||
borderRadius: '0.375rem',
|
||||
height: '100%',
|
||||
maxHeight: '100%',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: fluxStyling.backgroundColor + ' !important',
|
||||
resize: 'vertical',
|
||||
minHeight: '200px',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
borderTopLeftRadius: '0.375rem',
|
||||
backgroundColor: fluxStyling.gutterBackgroundColor + ' !important',
|
||||
},
|
||||
'.cm-gutter': {
|
||||
backgroundColor: fluxStyling.gutterBackgroundColor + ' !important',
|
||||
},
|
||||
'&.cm-focused': {
|
||||
outline: 'none',
|
||||
borderColor: fluxStyling.focusBorderColor,
|
||||
},
|
||||
'.cm-content': {
|
||||
padding: '12px',
|
||||
},
|
||||
'.cm-scroller': {
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
},
|
||||
'.cm-editor': {
|
||||
height: '100%',
|
||||
},
|
||||
'.cm-editor .cm-scroller': {
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
},
|
||||
'.cm-foldGutter': {
|
||||
width: '12px',
|
||||
},
|
||||
'.cm-foldGutter .cm-gutterElement': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
color: '#6b7280',
|
||||
},
|
||||
'.cm-foldGutter .cm-gutterElement:hover': {
|
||||
color: '#374151',
|
||||
},
|
||||
'.cm-foldGutter .cm-gutterElement.cm-folded': {
|
||||
color: '#3b82f6',
|
||||
}
|
||||
}),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
const newValue = update.state.doc.toString();
|
||||
onChange(newValue);
|
||||
onUpdate(newValue);
|
||||
}
|
||||
}),
|
||||
EditorView.domEventHandlers({
|
||||
blur: onBlur
|
||||
}),
|
||||
EditorView.editable.of(!readOnly),
|
||||
],
|
||||
parent: element
|
||||
});
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect system theme preference
|
||||
* @returns {string} 'dark' or 'light'
|
||||
*/
|
||||
export function getSystemTheme() {
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for system theme changes
|
||||
* @param {Function} callback - Callback function when theme changes
|
||||
* @returns {Function} Unwatch function
|
||||
*/
|
||||
export function watchThemeChange(callback) {
|
||||
if (window.matchMedia) {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
mediaQuery.addEventListener('change', callback);
|
||||
return () => mediaQuery.removeEventListener('change', callback);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
64
resources/views/livewire/codemirror.blade.php
Normal file
64
resources/views/livewire/codemirror.blade.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
use Livewire\Volt\Component;
|
||||
use Livewire\Attributes\Modelable;
|
||||
|
||||
new class extends Component
|
||||
{
|
||||
#[Modelable]
|
||||
public $model = '';
|
||||
public $language = 'html';
|
||||
public $theme = 'auto';
|
||||
public $readonly = false;
|
||||
public $placeholder = '';
|
||||
public $height = '200px';
|
||||
public $id = '';
|
||||
|
||||
public function mount($language = 'html', $theme = 'auto', $readonly = false, $placeholder = '', $height = '200px', $id = '')
|
||||
{
|
||||
$this->language = $language;
|
||||
$this->theme = $theme;
|
||||
$this->readonly = $readonly;
|
||||
$this->placeholder = $placeholder;
|
||||
$this->height = $height;
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
|
||||
public function toJSON()
|
||||
{
|
||||
return json_encode([
|
||||
'model' => $this->model,
|
||||
'language' => $this->language,
|
||||
'theme' => $this->theme,
|
||||
'readonly' => $this->readonly,
|
||||
'placeholder' => $this->placeholder,
|
||||
'height' => $this->height,
|
||||
'id' => $this->id,
|
||||
]);
|
||||
}
|
||||
} ?>
|
||||
|
||||
|
||||
<div
|
||||
x-data="codeMirrorComponent(@js($language), @js($theme), @js($readonly), @js($placeholder), @js($height), @js($id ?: uniqid()))"
|
||||
x-init="init()"
|
||||
wire:ignore
|
||||
class="codemirror-container"
|
||||
@if($id) id="{{ $id }}" @endif
|
||||
autocomplete="off"
|
||||
>
|
||||
<!-- Loading state -->
|
||||
<div x-show="isLoading" class="flex items-center justify-center p-4 border border-gray-300 rounded-md" style="height: {{ $height }};">
|
||||
<div class="flex items-center space-x-2 text-gray-500">
|
||||
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>Loading editor...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor container -->
|
||||
<div x-show="!isLoading" x-ref="editor" style="height: {{ $height }};"></div>
|
||||
</div>
|
||||
|
|
@ -1022,14 +1022,48 @@ HTML;
|
|||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<flux:label>Data Payload</flux:label>
|
||||
@isset($this->data_payload_updated_at)
|
||||
<flux:badge icon="clock" size="sm" variant="pill" class="ml-2">{{ $this->data_payload_updated_at?->diffForHumans() ?? 'Never' }}</flux:badge>
|
||||
@endisset
|
||||
<div class="mb-1">
|
||||
<flux:label>Data Payload</flux:label>
|
||||
@isset($this->data_payload_updated_at)
|
||||
<flux:badge icon="clock" size="sm" variant="pill" class="ml-2">{{ $this->data_payload_updated_at?->diffForHumans() ?? 'Never' }}</flux:badge>
|
||||
@endisset
|
||||
</div>
|
||||
<flux:error name="data_payload"/>
|
||||
<flux:textarea wire:model="data_payload" id="data_payload"
|
||||
class="block mt-1 w-full font-mono" type="text" name="data_payload"
|
||||
:readonly="$data_strategy !== 'static'" rows="24"/>
|
||||
<flux:field>
|
||||
@php
|
||||
$textareaId = 'payload-' . uniqid();
|
||||
@endphp
|
||||
<flux:textarea
|
||||
wire:model="data_payload"
|
||||
id="{{ $textareaId }}"
|
||||
placeholder="Enter your HTML code here..."
|
||||
rows="12"
|
||||
hidden
|
||||
/>
|
||||
<div
|
||||
x-data="codeEditorFormComponent({
|
||||
isDisabled: @js($data_strategy !== 'static'),
|
||||
language: 'json',
|
||||
state: $wire.entangle('data_payload'),
|
||||
textareaId: @js($textareaId)
|
||||
})"
|
||||
wire:ignore
|
||||
wire:key="cm-{{ $textareaId }}"
|
||||
class="max-w-2xl min-h-[300px] h-[500px] overflow-hidden resize-y"
|
||||
>
|
||||
<!-- Loading state -->
|
||||
<div x-show="isLoading" class="flex items-center justify-center h-full">
|
||||
<div class="flex items-center space-x-2 ">
|
||||
<flux:icon.loading />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor container -->
|
||||
<div x-show="!isLoading" x-ref="editor" class="h-full"></div>
|
||||
</div>
|
||||
</flux:field>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<flux:separator class="my-5"/>
|
||||
|
|
@ -1041,15 +1075,44 @@ HTML;
|
|||
<span class="font-mono text-accent mb-4">{{ $plugin->render_markup_view }}</span> to update.
|
||||
</div>
|
||||
<div class="mb-4 mt-4">
|
||||
<flux:textarea
|
||||
label="File Content"
|
||||
class="font-mono"
|
||||
wire:model="view_content"
|
||||
id="view_content"
|
||||
name="view_content"
|
||||
rows="15"
|
||||
readonly
|
||||
/>
|
||||
<flux:field>
|
||||
@php
|
||||
$textareaId = 'code-view-' . uniqid();
|
||||
@endphp
|
||||
<flux:label>{{ $markup_language === 'liquid' ? 'Liquid Code' : 'Blade Code' }}</flux:label>
|
||||
<flux:textarea
|
||||
wire:model="view_content"
|
||||
id="{{ $textareaId }}"
|
||||
placeholder="Enter your HTML code here..."
|
||||
rows="25"
|
||||
hidden
|
||||
/>
|
||||
<div
|
||||
x-data="codeEditorFormComponent({
|
||||
isDisabled: false,
|
||||
language: 'liquid',
|
||||
state: $wire.entangle('markup_code'),
|
||||
textareaId: @js($textareaId)
|
||||
})"
|
||||
wire:ignore
|
||||
wire:key="cm-{{ $textareaId }}"
|
||||
class="min-h-[300px] h-[300px] overflow-hidden resize-y"
|
||||
>
|
||||
<!-- Loading state -->
|
||||
<div x-show="isLoading" class="flex items-center justify-center h-full">
|
||||
<div class="flex items-center space-x-2">
|
||||
<flux:icon.loading />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor container -->
|
||||
<div x-show="!isLoading" x-ref="editor" class="h-full"></div>
|
||||
</div>
|
||||
</flux:field>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
@else
|
||||
<div class="flex items-center gap-6 mb-4 mt-4">
|
||||
|
|
@ -1071,15 +1134,41 @@ HTML;
|
|||
@if(!$plugin->render_markup_view)
|
||||
<form wire:submit="saveMarkup">
|
||||
<div class="mb-4">
|
||||
<flux:textarea
|
||||
label="{{ $markup_language === 'liquid' ? 'Liquid Code' : 'Blade Code' }}"
|
||||
class="font-mono"
|
||||
wire:model="markup_code"
|
||||
id="markup_code"
|
||||
name="markup_code"
|
||||
rows="15"
|
||||
placeholder="{{ $markup_language === 'liquid' ? 'Enter your liquid code here...' : 'Enter your blade code here...' }}"
|
||||
/>
|
||||
<flux:field>
|
||||
@php
|
||||
$textareaId = 'code-' . uniqid();
|
||||
@endphp
|
||||
<flux:label>{{ $markup_language === 'liquid' ? 'Liquid Code' : 'Blade Code' }}</flux:label>
|
||||
<flux:textarea
|
||||
wire:model="markup_code"
|
||||
id="{{ $textareaId }}"
|
||||
placeholder="Enter your HTML code here..."
|
||||
rows="25"
|
||||
hidden
|
||||
/>
|
||||
<div
|
||||
x-data="codeEditorFormComponent({
|
||||
isDisabled: false,
|
||||
language: 'liquid',
|
||||
state: $wire.entangle('markup_code'),
|
||||
textareaId: @js($textareaId)
|
||||
})"
|
||||
wire:ignore
|
||||
wire:key="cm-{{ $textareaId }}"
|
||||
class="min-h-[300px] h-[300px] overflow-hidden resize-y"
|
||||
>
|
||||
<!-- Loading state -->
|
||||
<div x-show="isLoading" class="flex items-center justify-center h-full">
|
||||
<div class="flex items-center space-x-2">
|
||||
<flux:icon.loading />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor container -->
|
||||
<div x-show="!isLoading" x-ref="editor" class="h-full"></div>
|
||||
</div>
|
||||
</flux:field>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue