Skip to content

Commit

Permalink
Merge pull request #30 from mailcarrierapp/feat/live-preview
Browse files Browse the repository at this point in the history
[2.x] Live preview
  • Loading branch information
danilopolani authored Apr 15, 2024
2 parents a0d4afa + 28d567c commit 7312ae5
Show file tree
Hide file tree
Showing 30 changed files with 664 additions and 55 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"laravel/socialite": "^5.6.1",
"livewire/livewire": "^3.4",
"nunomaduro/termwind": "^1.15|^2.0",
"pboivin/filament-peek": "^2.2",
"ralphjsmit/laravel-filament-components": "^2.0",
"socialiteproviders/manager": "^4.3",
"spatie/data-transfer-object": "^3.9.1",
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@
"build": "mix --production"
},
"devDependencies": {
"@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-json": "^6.0.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"autoprefixer": "^10.4.17",
"codemirror": "^6.0.1",
"highlight.js": "^11.9.0",
"laravel-mix": "^6.0.6",
"postcss": "^8.4.33",
"postcss-nesting": "^12.0.2",
"resolve-url-loader": "^5.0.0",
"sass": "^1.70.0",
"sass-loader": "^12.1.0",
"tailwindcss": "^3.4.1"
"tailwindcss": "^3.4.1",
"thememirror": "^2.0.1"
}
}
10 changes: 9 additions & 1 deletion resources/css/theme.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import '../../vendor/filament/filament/resources/css/theme.css';
@import "../../vendor/filament/filament/resources/css/theme.css";

@config '../../tailwind.config.js';

Expand All @@ -7,3 +7,11 @@
top: -2px;
transform: rotate(-40deg);
}

.filament-peek-panel-body iframe {
@apply rounded bg-white;
}

.cm-editor {
@apply h-full;
}
4 changes: 2 additions & 2 deletions resources/dist/css/theme.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions resources/dist/js/codemirror.component.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions resources/dist/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"/js/highlight.js": "/js/highlight.js",
"/js/codemirror.component.js": "/js/codemirror.component.js",
"/css/theme.css": "/css/theme.css",
"/css/highlight.css": "/css/highlight.css"
}
86 changes: 86 additions & 0 deletions resources/js/codemirror.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
history,
indentWithTab
} from "@codemirror/commands";
import { html } from "@codemirror/lang-html";
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from "@codemirror/view";
import { dracula, smoothy } from 'thememirror';

// Adapted from https://github.com/dotswan/filament-code-editor
const CodeEditorAlpinePlugin = (Alpine) => {
Alpine.data('codeEditorFormComponent', ({ state, isReadOnly, language = 'html' }) => ({
state,
editor: undefined,
themeConfig: undefined,
languageConfig: undefined,
isReadOnly: false,

init() {
this.isReadOnly = isReadOnly;
this.themeConfig = new Compartment();
this.languageConfig = new Compartment();
this.render();

// Needed for programmatic updates from Livewire (e.g. form fill) to the component
this.$watch('state', (value) => {
if (this.editor.state.doc.toString() !== value) {
this.editor.dispatch({
changes: { from: 0, to: this.editor.state.doc.length, insert: value }
});
}
});
},

render() {
this.editor = new EditorView({
parent: this.$refs.codeEditor,
state: EditorState.create({
doc: this.state,
autofocus: true,
indentWithTabs: true,
smartIndent: true,
lineNumbers: true,
matchBrackets: true,
tabSize: 2,
styleSelectedText: true,
extensions: [
keymap.of([indentWithTab]),
this.languageConfig.of(language === 'json' ? json() : html()),
this.themeConfig.of([dracula]),
EditorView.lineWrapping,
EditorState.readOnly.of(this.isReadOnly),
lineNumbers(),
history(),
highlightActiveLineGutter(),
EditorView.updateListener.of((v) => {
if (v.docChanged) {
this.state = v.state.doc.toString();
this.$wire.$commit();
}
}),
],
}),
});

window.addEventListener('theme-changed', () => {
let theme = localStorage.getItem('theme');
if (theme === 'system') {
theme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
? 'dark'
: 'light';
}

this.editor.dispatch({
effects: this.themeConfig.reconfigure([
theme === 'light' ? smoothy : dracula
])
});
});
},
}));
}

document.addEventListener('alpine:init', () => {
window.Alpine.plugin(CodeEditorAlpinePlugin);
});
50 changes: 14 additions & 36 deletions resources/views/forms/components/code-editor.blade.php
Original file line number Diff line number Diff line change
@@ -1,39 +1,17 @@
{{-- Adapted from https://github.com/dotswan/filament-code-editor --}}
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
<div x-data="codeEditor" class="w-full max-h-full rounded-lg shadow-sm ring-1 transition duration-75 bg-white dark:bg-white/5 [&:not(:has(.fi-ac-action:focus))]:focus-within:ring-2 ring-gray-950/10 dark:ring-white/20 [&:not(:has(.fi-ac-action:focus))]:focus-within:ring-primary-600 dark:[&:not(:has(.fi-ac-action:focus))]:focus-within:ring-primary-500 overflow-hidden">
<div x-ref="editor" class="w-full h-[300px]" wire:ignore></div>
<div class="relative max-w-full overflow-hidden rounded-lg shadow-sm ring-1 transition duration-75 bg-white dark:bg-white/5 [&:not(:has(.fi-ac-action:focus))]:focus-within:ring-2 ring-gray-950/10 dark:ring-white/20 [&:not(:has(.fi-ac-action:focus))]:focus-within:ring-primary-600 dark:[&:not(:has(.fi-ac-action:focus))]:focus-within:ring-primary-500 h-[365px]">
<div
class="w-full h-full"
x-data="codeEditorFormComponent({
state: $wire.$entangle('{{ $getStatePath() }}'),
isReadOnly: @js($isDisabled()),
})">
<div
wire:ignore
x-ref="codeEditor"
class="w-full h-full">
</div>
</div>
</div>
</x-dynamic-component>

<script type="module">
import * as monaco from 'https://cdn.jsdelivr.net/npm/[email protected]/+esm';
Alpine.data('codeEditor', () => ({
state: @entangle($getStatePath()),
init() {
const editor = monaco.editor.create(this.$refs.editor, {
value: this.state,
language: 'twig',
automaticLayout: true,
scrollBeyondLastLine: false,
readOnly: {{ $isDisabled() ? 'true' : 'false' }},
minimap: {
enabled: false,
},
});
editor.getModel().onDidChangeContent(() => this.state = editor.getValue());
window.addEventListener('theme-changed', () => {
let theme = localStorage.getItem('theme');
if (theme === 'system') {
theme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)
? 'dark'
: 'light';
}
monaco.editor.setTheme(theme === 'light' ? 'vs' : 'vs-dark');
});
}
}));
</script>
26 changes: 26 additions & 0 deletions resources/views/livewire/layout.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8" />

<meta name="application-name" content="{{ config('app.name') }}" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf_token" value="{{ csrf_token() }}"/>

<title>MailCarrier preview</title>

<style>
[x-cloak] {
display: none !important;
}
</style>

@livewireStyles
</head>

<body class="antialiased">
{{ $slot }}

@livewireScripts
</body>
</html>
43 changes: 43 additions & 0 deletions resources/views/livewire/preview/builder-editor.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<div
class="filament-peek-panel filament-peek-editor"
x-bind:style="editorStyle"
x-ref="builderEditor"
@if ($this->canAutoRefresh()) data-auto-refresh-strategy="{{ $this->autoRefreshStrategy }}" @endif
@if ($this->shouldAutoRefresh()) data-should-auto-refresh="1" @endif
>
<div class="filament-peek-panel-header">
<div x-text="editorTitle"></div>
</div>

<div
class="filament-peek-panel-body"
x-on:focusout="onEditorFocusOut($event)"
>
<div
x-bind:class="{
'filament-peek-builder-editor': true,
'has-sidebar-actions': editorHasSidebarActions,
}"
>
<div class="filament-peek-builder-content">
<form wire:submit="submit">
{{ $this->form }}

<button type="submit" style="display: none">
{{ __('filament-peek::ui.refresh-action-label') }}
</button>
</form>

<x-filament-actions::modals />
</div>

<div class="filament-peek-builder-actions"></div>

<div
class="filament-peek-editor-resizer"
x-on:mousedown="onEditorResizerMouseDown($event)"
x-bind:style="{display: editorIsResizable ? 'initial' : 'none'}"
></div>
</div>
</div>
</div>
62 changes: 62 additions & 0 deletions resources/views/livewire/preview/partials/modal-actions.blade.php

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions resources/views/livewire/preview/preview-modal.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
@if (\Pboivin\FilamentPeek\Support\View::needsPreviewModal())
<div
role="alertdialog"
aria-modal="true"
aria-labelledby="filament-peek-modal-title"
x-data="PeekPreviewModal({
devicePresets: @js(config('filament-peek.devicePresets', false)),
initialDevicePreset: @js(config('filament-peek.initialDevicePreset', 'fullscreen')),
allowIframeOverflow: @js(config('filament-peek.allowIframeOverflow', false)),
shouldCloseModalWithEscapeKey: @js(config('filament-peek.closeModalWithEscapeKey', true)),
editorAutoRefreshDebounceTime: @js(config('filament-peek.builderEditor.autoRefreshDebounceMilliseconds', 500)),
shouldRestoreIframePositionOnRefresh: @js(config('filament-peek.builderEditor.preservePreviewScrollPosition', false)),
canResizeEditorSidebar: @js(config('filament-peek.builderEditor.canResizeSidebar', true)),
editorSidebarMinWidth: @js(config('filament-peek.builderEditor.sidebarMinWidth', '30rem')),
editorSidebarInitialWidth: @js(config('filament-peek.builderEditor.sidebarInitialWidth', '30rem')),
})"
x-bind:class="{
'filament-peek-modal': true,
'is-filament-peek-editor-resizing': editorIsResizing,
}"
x-bind:style="modalStyle"
x-on:open-preview-modal.window="onOpenPreviewModal($event)"
x-on:refresh-preview-modal.window="onRefreshPreviewModal($event)"
x-on:close-preview-modal.window="onClosePreviewModal($event)"
x-on:keyup.escape.window="handleEscapeKey()"
x-on:mouseup.window="onMouseUp($event)"
x-on:mousemove.debounce.5ms.window="onMouseMove($event)"
x-trap="isOpen"
x-cloak
>
@if (\Pboivin\FilamentPeek\Support\View::needsBuilderEditor())
@livewire('filament-peek::builder-editor')
@endif

<div class="filament-peek-panel filament-peek-preview">
<div class="filament-peek-panel-header">
<div
id="filament-peek-modal-title"
class="filament-peek-modal-title"
x-text="modalTitle"
></div>

@if (config('filament-peek.devicePresets', false))
<div class="filament-peek-device-presets">
@foreach (config('filament-peek.devicePresets') as $presetName => $presetConfig)
<button
type="button"
data-preset-name="{{ $presetName }}"
x-on:click="setDevicePreset('{{ $presetName }}')"
x-bind:class="{'is-active-device-preset': isActiveDevicePreset('{{ $presetName }}')}"
>
<x-filament::icon
:icon="$presetConfig['icon'] ?? 'heroicon-o-computer-desktop'"
:class="Arr::toCssClasses(['rotate-90' => $presetConfig['rotateIcon'] ?? false])"
/>
</button>
@endforeach

<button
type="button"
class="filament-peek-rotate-preset"
x-on:click="rotateDevicePreset()"
x-bind:disabled="!canRotatePreset"
>
@include('filament-peek::partials.icon-rotate')
</button>
</div>
@endif

<div class="filament-peek-modal-actions">
@include('mailcarrier::livewire.preview.partials.modal-actions')
</div>
</div>

<div
x-ref="previewModalBody"
class="{{ Arr::toCssClasses([
'filament-peek-panel-body' => true,
'allow-iframe-overflow' => config('filament-peek.allowIframeOverflow', false),
]) }}"
>
<template x-if="iframeUrl">
<iframe
x-bind:src="iframeUrl"
x-bind:style="iframeStyle"
frameborder="0"
></iframe>
</template>

<template x-if="!iframeUrl && iframeContent">
<iframe
x-bind:srcdoc="iframeContent"
x-bind:style="iframeStyle"
frameborder="0"
></iframe>
</template>

<div class="filament-peek-iframe-cover"></div>
</div>
</div>
</div>
@endif
7 changes: 6 additions & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
use MailCarrier\Http\Controllers\LogController;
use MailCarrier\Http\Controllers\MailCarrierController;
use MailCarrier\Http\Controllers\SocialAuthController;
use MailCarrier\Livewire\PreviewTemplate;

Route::middleware(['web', 'auth:' . Config::get('filament.auth.guard')])->group(function () {
Route::get('preview/logs/{log}', [LogController::class, 'preview'])->name('logs.preview');
Route::get('logs/{log}/preview', [LogController::class, 'preview'])->name('logs.preview');
Route::get('attachment/{attachment}', [MailCarrierController::class, 'downloadAttachment'])
->whereUuid('attachment')
->name('download.attachment');
Expand All @@ -17,3 +18,7 @@
Route::get('redirect', [SocialAuthController::class, 'redirect'])->name('auth.redirect');
Route::get('callback', [SocialAuthController::class, 'callback'])->name('auth.callback');
});

Route::middleware('web')->group(function () {
Route::get('templates/preview', PreviewTemplate::class)->name('templates.preview');
});
Loading

0 comments on commit 7312ae5

Please sign in to comment.