├── .prettierrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── filament-peek.php ├── generate-toc.php ├── package-lock.json ├── package.json ├── postcss.config.js ├── resources ├── css │ ├── internal.css │ └── plugin.css ├── dist │ ├── filament-peek.css │ └── filament-peek.js ├── js │ └── plugin.js ├── lang │ ├── ar │ │ └── ui.php │ ├── cs │ │ └── ui.php │ ├── en │ │ └── ui.php │ ├── es │ │ └── ui.php │ ├── fr │ │ └── ui.php │ ├── nl │ │ └── ui.php │ └── tr │ │ └── ui.php └── views │ ├── components │ └── preview-link.blade.php │ ├── livewire │ └── builder-editor.blade.php │ ├── partials │ ├── icon-rotate.blade.php │ └── modal-actions.blade.php │ └── preview-modal.blade.php ├── routes └── preview.php ├── src ├── CachedBuilderPreview.php ├── CachedPreview.php ├── Exceptions │ └── PreviewModalException.php ├── FilamentPeekPlugin.php ├── FilamentPeekServiceProvider.php ├── Forms │ ├── Actions │ │ └── InlinePreviewAction.php │ └── Components │ │ └── PreviewLink.php ├── Livewire │ └── BuilderEditor.php ├── Pages │ ├── Actions │ │ └── PreviewAction.php │ └── Concerns │ │ ├── HasBuilderPreview.php │ │ └── HasPreviewModal.php ├── Support │ ├── Cache.php │ ├── Concerns │ │ ├── CanPreviewInNewTab.php │ │ └── SetsInitialPreviewModalData.php │ ├── Html.php │ ├── Page.php │ ├── Panel.php │ └── View.php └── Tables │ └── Actions │ └── ListPreviewAction.php └── tailwind.config.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `filament-peek` will be documented in this file. 4 | 5 | 6 | ## 2.4.0 - 2025-03-14 7 | 8 | * feat: Open preview in new browser tab 9 | 10 | 11 | ## 2.3.0 - 2025-02-25 12 | 13 | * chore: Laravel 12.x Compatibility 14 | * chore: Update Github Actions 15 | * chore: bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 16 | * chore: bump aglipanci/laravel-pint-action from 2.4 to 2.5 17 | 18 | 19 | ## 2.2.11 - 2025-01-03 20 | 21 | * enh: Add Turkish translations by @AzizEmir 22 | * fix: Use FQCN for Arr in Blade views 23 | 24 | 25 | ## 2.2.10 - 2024-10-15 26 | 27 | * fix: Remove tailwind utils from plugin.css 28 | 29 | 30 | ## 2.2.9 - 2024-09-04 31 | 32 | * enh: Add Czech translations by @JarkaP 33 | 34 | 35 | ## 2.2.8 - 2024-08-29 36 | 37 | * enh: Add Dutch translations by @el-klo 38 | * chore: bump dependabot/fetch-metadata from 2.1.0 to 2.2.0 39 | 40 | 41 | ## 2.2.7 - 2024-05-03 42 | 43 | * fix: Remove filament vendor views from tailwind content config 44 | * enh: Fix preview styles on mobile screens 45 | * chore: bump dependabot/fetch-metadata from 2.0.0 to 2.1.0 46 | 47 | 48 | ## 2.2.6 - 2024-04-24 49 | 50 | * enh: Support ListPreviewAction with relation managers 51 | * chore: bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 52 | * chore: bump aglipanci/laravel-pint-action from 2.3.1 to 2.4 53 | 54 | 55 | ## 2.2.5 - 2024-03-11 56 | 57 | * chore: Add Laravel 11 compatibility by @bambamboole 58 | * chore: bump ramsey/composer-install from 2 to 3 59 | 60 | 61 | ## 2.2.4 - 2024-03-02 62 | 63 | * fix: Fix iframe not refreshing when using internalPreviewUrl by @FDT2k 64 | 65 | 66 | ## 2.2.3 - 2024-01-31 67 | 68 | * fix: Call beforeStateDehydrated hook by default to handle image uploads 69 | 70 | 71 | ## 2.2.2 - 2024-01-13 72 | 73 | * fix: Don't call state hooks by default when opening the preview modal 74 | * chore: Fix tests 75 | * chore: PHP 8.3 76 | * chore: bump aglipanci/laravel-pint-action from 2.3.0 to 2.3.1 77 | 78 | 79 | ## 2.2.1 - 2023-11-13 80 | 81 | * enh: Spanish translations by @kennyhorna 82 | 83 | 84 | ## 2.2.0 - 2023-10-10 85 | 86 | * feat: Add option to use internal preview URL 87 | * chore: Bump stefanzweifel/git-auto-commit-action 88 | 89 | 90 | ## 2.1.0 - 2023-09-24 91 | 92 | * feat: Add ListPreviewAction component 93 | * feat: Add preview modal data from PreviewAction 94 | * enh: Add builderName() method alias 95 | * docs: Update documentation 96 | 97 | 98 | ## 2.0.2 - 2023-09-16 99 | 100 | * fix: Prevent validation issue with auto-refresh option 101 | * enh: Prevent multiple refreshes in one request 102 | 103 | 104 | ## 2.0.1 - 2023-09-16 105 | 106 | * fix: Ensure builder field is remembered in InlinePreviewAction 107 | * chore: Rework integration tests 108 | * chore: Update run-tests workflow 109 | * chore: Bump actions/checkout 110 | 111 | 112 | ## 2.0.0 - 2023-08-31 113 | 114 | Stable release 115 | 116 | 117 | ## 2.0.0-beta4 - 2023-08-26 118 | 119 | * fix: Don't attach keyup listener if not needed 120 | * fix: Try/catch iframe operations 121 | 122 | 123 | ## 2.0.0-beta3 - 2023-08-25 124 | 125 | * enh: Replace assets config with plugin methods 126 | * docs: Add documentation on custom theme integration 127 | 128 | 129 | ## 2.0.0-beta2 - 2023-08-12 130 | 131 | * feat: Add InlinePreviewAction (Deprecate PreviewLink) 132 | * fix: Add missing modal tag 133 | * fix: Dark mode styles 134 | * chore: Update illuminate/contracts requirement 135 | 136 | 137 | ## 2.0.0-beta1 - 2023-07-31 138 | 139 | * fix: Refresh on render if needed 140 | 141 | 142 | ## 2.0.0-alpha1 - 2023-07-30 143 | 144 | * feat!: Initial support for Filament 3 145 | 146 | 147 | ## 1.1.1 - 2023-08-26 148 | 149 | * fix: Don't attach keyup listener if not needed 150 | * fix: Try/catch iframe operations 151 | 152 | 153 | ## 1.1.0 - 2023-07-26 154 | 155 | * feat: Add preview modal JavaScript hooks 156 | 157 | 158 | ## 1.0.2 - 2023-07-23 159 | 160 | * fix: Editor sidebar resize in RTL UI 161 | 162 | 163 | ## 1.0.1 - 2023-07-18 164 | 165 | * enh: Add Arabic translation by @atmonshi 166 | 167 | 168 | ## 1.0.0 - 2023-07-16 169 | 170 | * enh: Default canDiscardChanges to true 171 | * fix: Fill builder editor data 172 | 173 | 174 | ## 1.0.0-beta2 - 2023-07-15 175 | 176 | * refactor: Always show active preset 177 | * refactor: Updade config options 178 | * fix: Prevent crash in resetBuilderEditor 179 | * fix: Validate form before preview 180 | * fix: Validate builder editor before closing preview 181 | * test: Add BuilderEditorTest 182 | 183 | 184 | ## 1.0.0-beta1 - 2023-07-09 185 | * feat: Add 'reactive' auto refresh strategy 186 | * feat: Add option to restore iframe scroll position on refresh 187 | * enh: Update getListeners method 188 | * enh: Add check for missing custom event listener 189 | * enh: Accept single Component as builder schema 190 | * enh: Throw custom exception if page is not properly configured 191 | * enh: Refresh on submit 192 | * enh: Improve Tiptap support 193 | * fix: Builder editor improvements 194 | * fix: Pass raw editor data to mutateBuilderPreviewData 195 | * fix: Update builder field if empty 196 | * fix: Improve close modal handling 197 | * chore: Bump dependabot/fetch-metadata from 1.5.1 to 1.6.0 198 | 199 | 200 | ## 1.0.0-alpha2 - 2023-07-04 201 | 202 | * refactor!: Update various method names 203 | * feat: Implement sidebar resize 204 | * feat: Support custom focus out handlers 205 | * enh: Detect if editor has sidebar actions 206 | * enh: Update PreviewLink default styles 207 | * fix: Support preview modal on View pages 208 | 209 | 210 | ## 1.0.0-alpha1 - 2023-06-23 211 | 212 | * feat: Builder Previews 213 | * docs: Builder Previews Documentation 214 | * enh: Add type annotations and tag internal methods 215 | 216 | 217 | ## 0.3.1 - 2023-06-26 218 | 219 | * fix: Support preview modal on View pages 220 | 221 | 222 | ## 0.3.0 - 2023-06-11 223 | 224 | * feat: Show active device preset 225 | * feat: Add closeModalWithEscapeKey config 226 | * refactor: Extract alpine component and version dist assets 227 | 228 | 229 | ## 0.2.4 - 2023-06-10 230 | 231 | * fix: Handle escape key within preview modal iframe 232 | * enh: Update preview modal pointer-events CSS selector 233 | 234 | 235 | ## 0.2.3 - 2023-06-05 236 | 237 | * fix: Replace iframe pointer-events CSS with preview modal content style block 238 | 239 | 240 | ## 0.2.2 - 2023-05-30 241 | 242 | - enh: Extract renderPreviewModalView method 243 | - chore: Bump dependabot/fetch-metadata from 1.5.0 to 1.5.1 244 | - chore: Bump aglipanci/laravel-pint-action from 2.2.0 to 2.3.0 245 | 246 | 247 | ## 0.2.1 - 2023-05-28 248 | 249 | - fix: Remove duplicated call to getPreviewModalUrl 250 | - test: Add HasPreviewModal tests 251 | 252 | 253 | ## 0.2.0 - 2023-05-26 254 | 255 | - feat: Add pointer events config 256 | - feat: Add focus trap and handle escape key 257 | - fix: Handle preset rotation when using allowIframeOverflow config 258 | - enh: Change PreviewLink into form component 259 | 260 | 261 | ## 0.1.2 - 2023-05-22 262 | 263 | - fix: Support preview modal on List and Create pages 264 | 265 | 266 | ## 0.1.1 - 2023-05-22 267 | 268 | - fix: Remove unused method 269 | - chore: Bump dependabot/fetch-metadata from 1.4.0 to 1.5.0 270 | 271 | 272 | ## 0.1.0 - 2023-05-22 273 | 274 | - Initial release 275 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) pboivin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Peek 2 | 3 |

4 | Build Status 5 | Latest Stable Version 6 | Total Downloads 7 | License 8 |

9 | 10 | A Filament plugin to add a full-screen preview modal to your Panel pages. The modal can be used before saving to preview a modified record. 11 | 12 |

13 | Screenshots of the edit page and preview modal 14 |

15 | 16 | ## Installation 17 | 18 | You can install the package via composer: 19 | 20 | ```bash 21 | composer require pboivin/filament-peek:"^2.0" 22 | ``` 23 | 24 | Register a `FilamentPeekPlugin` instance in your Panel provider: 25 | 26 | ```php 27 | use Pboivin\FilamentPeek\FilamentPeekPlugin; 28 | 29 | public function panel(Panel $panel): Panel 30 | { 31 | return $panel 32 | // ... 33 | ->plugins([ 34 | FilamentPeekPlugin::make(), 35 | ]); 36 | } 37 | ``` 38 | 39 | Then, publish the assets: 40 | 41 | ```bash 42 | php artisan filament:assets 43 | ``` 44 | 45 | #### Upgrading from `1.x` 46 | 47 | Follow the steps in the [Upgrade Guide](https://github.com/pboivin/filament-peek/tree/2.x/docs/upgrade-guide.md). 48 | 49 | ## Compatibility 50 | 51 | | Peek | Status | Filament | PHP | 52 | |------|----------|-----|--------| 53 | | [1.x](https://github.com/pboivin/filament-peek/tree/1.x) | Bugfixes only | ^2.0 | ^8.0 | 54 | | [2.x](https://github.com/pboivin/filament-peek/tree/2.x) | Current version | ^3.0 | ^8.1 | 55 | 56 | Please feel free to report any issues you encounter with Peek [in this repository](https://github.com/pboivin/filament-peek/issues). I'll work with you to determine where the issue is coming from. 57 | 58 | ## Demo Projects 59 | 60 | Here are a few example projects available to give this plugin a try: 61 | 62 | | Repository | Description | 63 | |------|----------| 64 | | [filament-peek-demo](https://github.com/pboivin/filament-peek-demo) | Content previews on a simple Filament project with Laravel Blade views. | 65 | | [filament-peek-demo-with-astro](https://github.com/pboivin/filament-peek-demo-with-astro) | Content previews on a more complex project with Filament as "headless CMS", and [Astro](https://astro.build/) on the front-end. (Archived) | 66 | | [Log1x/filament-starter](https://github.com/Log1x/filament-starter) | A great starting point for TALL stack projects using Filament. Implements content previews using full-page Livewire components. | 67 | 68 | ## Documentation 69 | 70 | The documentation is available in the ['docs' directory](https://github.com/pboivin/filament-peek/tree/2.x/docs) on GitHub: 71 | 72 | 73 | 74 | - [Configuration](https://github.com/pboivin/filament-peek/blob/2.x/docs/configuration.md) 75 | - [Publishing the Config File](https://github.com/pboivin/filament-peek/blob/2.x/docs/configuration.md#publishing-the-config-file) 76 | - [Available Options](https://github.com/pboivin/filament-peek/blob/2.x/docs/configuration.md#available-options) 77 | - [Integrating With a Custom Theme](https://github.com/pboivin/filament-peek/blob/2.x/docs/configuration.md#integrating-with-a-custom-theme) 78 | - [Page Previews](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md) 79 | - [Overview](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md#overview) 80 | - [Using the Preview Modal on Edit pages](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md#using-the-preview-modal-on-edit-pages) 81 | - [Using the Preview Modal on List pages](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md#using-the-preview-modal-on-list-pages) 82 | - [Detecting the Preview Modal](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md#detecting-the-preview-modal) 83 | - [Using a Preview URL](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md#using-a-preview-url) 84 | - [Embedding a Preview Action into the Form](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md#embedding-a-preview-action-into-the-form) 85 | - [Preview Pointer Events](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md#preview-pointer-events) 86 | - [Adding Extra Data to Previews](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md#adding-extra-data-to-previews) 87 | - [Alternate Templating Engines](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md#alternate-templating-engines) 88 | - [Opening the Preview in a New Tab](https://github.com/pboivin/filament-peek/blob/2.x/docs/page-previews.md#opening-the-preview-in-a-new-tab) 89 | - [Builder Previews](https://github.com/pboivin/filament-peek/blob/2.x/docs/builder-previews.md) 90 | - [Overview](https://github.com/pboivin/filament-peek/blob/2.x/docs/builder-previews.md#overview) 91 | - [Using Builder Previews on Edit pages](https://github.com/pboivin/filament-peek/blob/2.x/docs/builder-previews.md#using-builder-previews-on-edit-pages) 92 | - [Using Multiple Builder Fields](https://github.com/pboivin/filament-peek/blob/2.x/docs/builder-previews.md#using-multiple-builder-fields) 93 | - [Using Custom Fields](https://github.com/pboivin/filament-peek/blob/2.x/docs/builder-previews.md#using-custom-fields) 94 | - [Customizing the Preview Action](https://github.com/pboivin/filament-peek/blob/2.x/docs/builder-previews.md#customizing-the-preview-action) 95 | - [Automatically Updating the Builder Preview](https://github.com/pboivin/filament-peek/blob/2.x/docs/builder-previews.md#automatically-updating-the-builder-preview) 96 | - [Adding Extra Data to the Builder Editor State](https://github.com/pboivin/filament-peek/blob/2.x/docs/builder-previews.md#adding-extra-data-to-the-builder-editor-state) 97 | - [Adding Extra Data to the Builder Preview](https://github.com/pboivin/filament-peek/blob/2.x/docs/builder-previews.md#adding-extra-data-to-the-builder-preview) 98 | - [Alternate Templating Engines](https://github.com/pboivin/filament-peek/blob/2.x/docs/builder-previews.md#alternate-templating-engines) 99 | - [JavaScript Hooks](https://github.com/pboivin/filament-peek/blob/2.x/docs/javascript-hooks.md) 100 | - [Upgrading from v1.x](https://github.com/pboivin/filament-peek/blob/2.x/docs/upgrade-guide.md) 101 | 102 | 103 | 104 | ## FAQ and Known Issues 105 | 106 | I've started compiling some notes and solutions to common issues in [Discussions](https://github.com/pboivin/filament-peek/discussions/categories/general). Feel free to contribute your own tips and tricks. 107 | 108 | ## Changelog 109 | 110 | Please see [CHANGELOG](https://github.com/pboivin/filament-peek/blob/2.x/CHANGELOG.md) for more information on what has changed recently. 111 | 112 | ## Contributing 113 | 114 | Please see [CONTRIBUTING](https://github.com/pboivin/filament-peek/blob/2.x/.github/CONTRIBUTING.md) for details. 115 | 116 | ## Security Vulnerabilities 117 | 118 | Please review [our security policy](https://github.com/pboivin/filament-peek/security/policy) on how to report security vulnerabilities. 119 | 120 | ## Credits 121 | 122 | - [Patrick Boivin](https://github.com/pboivin) 123 | - [All Contributors](https://github.com/pboivin/filament-peek/contributors) 124 | 125 | ## Acknowledgements 126 | 127 | The initial idea is heavily inspired by module previews in [Twill CMS](https://twillcms.com/). 128 | 129 | ## License 130 | 131 | The MIT License (MIT). Please see [License File](https://github.com/pboivin/filament-peek/blob/2.x/LICENSE.md) for more information. 132 | 133 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pboivin/filament-peek", 3 | "description": "Full-screen page preview modal for Filament", 4 | "keywords": [ 5 | "laravel", 6 | "filament", 7 | "plugin", 8 | "preview", 9 | "previewer", 10 | "modal" 11 | ], 12 | "homepage": "https://github.com/pboivin/filament-peek", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Patrick Boivin", 17 | "email": "pboivin@gmail.com", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "php": "^8.1", 23 | "filament/filament": "^3.0", 24 | "spatie/laravel-package-tools": "^1.15", 25 | "illuminate/contracts": "^10.0 || ^11.0 || ^12.0" 26 | }, 27 | "require-dev": { 28 | "laravel/pint": "^1.0", 29 | "nunomaduro/collision": "^7.9 || ^8.0", 30 | "larastan/larastan": "^2.0 || ^3.0", 31 | "orchestra/testbench": "^8.0 || ^9.0 || ^10.0", 32 | "pestphp/pest": "^2.0 || ^3.7", 33 | "pestphp/pest-plugin-arch": "^2.0 || ^3.0", 34 | "pestphp/pest-plugin-laravel": "^2.0 || ^3.1", 35 | "phpstan/extension-installer": "^1.1", 36 | "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", 37 | "phpstan/phpstan-phpunit": "^1.0 || ^2.0", 38 | "spatie/invade": "^1.1 || ^2.1", 39 | "symfony/polyfill-php82": "^1.28" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Pboivin\\FilamentPeek\\": "src" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Pboivin\\FilamentPeek\\Tests\\": "tests/src", 49 | "Pboivin\\FilamentPeek\\Tests\\Database\\Factories\\": "tests/database/factories" 50 | } 51 | }, 52 | "scripts": { 53 | "pint": "vendor/bin/pint", 54 | "test:pest": "vendor/bin/pest --parallel", 55 | "test:phpstan": "vendor/bin/phpstan analyse", 56 | "test": [ 57 | "@test:pest", 58 | "@test:phpstan" 59 | ] 60 | }, 61 | "config": { 62 | "sort-packages": true, 63 | "allow-plugins": { 64 | "composer/package-versions-deprecated": true, 65 | "pestphp/pest-plugin": true, 66 | "phpstan/extension-installer": true 67 | } 68 | }, 69 | "extra": { 70 | "laravel": { 71 | "providers": [ 72 | "Pboivin\\FilamentPeek\\FilamentPeekServiceProvider" 73 | ] 74 | } 75 | }, 76 | "minimum-stability": "dev", 77 | "prefer-stable": true 78 | } 79 | -------------------------------------------------------------------------------- /config/filament-peek.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'fullscreen' => [ 17 | 'icon' => 'heroicon-o-computer-desktop', 18 | 'width' => '100%', 19 | 'height' => '100%', 20 | 'canRotatePreset' => false, 21 | ], 22 | 'tablet-landscape' => [ 23 | 'icon' => 'heroicon-o-device-tablet', 24 | 'rotateIcon' => true, 25 | 'width' => '1080px', 26 | 'height' => '810px', 27 | 'canRotatePreset' => true, 28 | ], 29 | 'mobile' => [ 30 | 'icon' => 'heroicon-o-device-phone-mobile', 31 | 'width' => '375px', 32 | 'height' => '667px', 33 | 'canRotatePreset' => true, 34 | ], 35 | ], 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Initial Device Preset 40 | |-------------------------------------------------------------------------- 41 | | 42 | | The default device preset to be activated when the modal is open. 43 | | 44 | */ 45 | 46 | 'initialDevicePreset' => 'fullscreen', 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Allow Iframe Overflow 51 | |-------------------------------------------------------------------------- 52 | | 53 | | Set this to `true` to allow the iframe dimensions to go beyond the 54 | | capacity of the available preview modal area. 55 | | 56 | */ 57 | 58 | 'allowIframeOverflow' => false, 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Allow Iframe Pointer Events 63 | |-------------------------------------------------------------------------- 64 | | 65 | | Set this to `true` to allow all pointer events (clicks, etc.) within the 66 | | iframe. By default, only scrolling is allowed. 67 | | 68 | */ 69 | 70 | 'allowIframePointerEvents' => false, 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Close Modal With Escape Key 75 | |-------------------------------------------------------------------------- 76 | | 77 | | Set this to `false` to reserve the Escape key for the purposes of your 78 | | page preview. This option does not apply to Builder previews. 79 | | 80 | */ 81 | 82 | 'closeModalWithEscapeKey' => true, 83 | 84 | /* 85 | |-------------------------------------------------------------------------- 86 | | Internal Preview URL 87 | |-------------------------------------------------------------------------- 88 | | 89 | | Enable this option to render all Blade previews through an internal URL. 90 | | This improves the isolation of the iframe in the context of the page. 91 | | Add additional middleware for this URL in the `middleware` array. 92 | | 93 | */ 94 | 95 | 'internalPreviewUrl' => [ 96 | 'enabled' => false, 97 | 'middleware' => [], 98 | 'cacheDuration' => 60, 99 | ], 100 | 101 | /* 102 | |-------------------------------------------------------------------------- 103 | | Builder Editor 104 | |-------------------------------------------------------------------------- 105 | | 106 | | Options related to the Editor sidebar in Builder Previews. 107 | | 108 | */ 109 | 110 | 'builderEditor' => [ 111 | 112 | // Show 'Accept' and 'Discard' buttons in modal header instead of a single 'Close' button. 113 | 'canDiscardChanges' => true, 114 | 115 | // Allow users to resize the sidebar by clicking and dragging on the right edge. 116 | 'canResizeSidebar' => true, 117 | 118 | // Minimum width for the sidebar, if resizable. Must be a valid CSS value. 119 | 'sidebarMinWidth' => '30rem', 120 | 121 | // Initial width for the sidebar. Must be a valid CSS value. 122 | 'sidebarInitialWidth' => '30rem', 123 | 124 | // Restore the preview scroll position when the preview is refreshed. 125 | 'preservePreviewScrollPosition' => true, 126 | 127 | // Enable the auto-refresh option for the Builder preview. 128 | 'canEnableAutoRefresh' => true, 129 | 130 | // Debounce time before refreshing the preview. 131 | 'autoRefreshDebounceMilliseconds' => 500, 132 | 133 | // Possible values: 'simple' or 'reactive'. 134 | 'autoRefreshStrategy' => 'simple', 135 | 136 | // Livewire component for the sidebar. 137 | 'livewireComponentClass' => \Pboivin\FilamentPeek\Livewire\BuilderEditor::class, 138 | 139 | ], 140 | 141 | ]; 142 | -------------------------------------------------------------------------------- /generate-toc.php: -------------------------------------------------------------------------------- 1 | ', 39 | '', 40 | ...$toc, 41 | '', 42 | '', 43 | ]); 44 | } 45 | 46 | function generateFooter(string $prefix): string 47 | { 48 | $toc = []; 49 | 50 | foreach (DOC_FILES as $file) { 51 | foreach (file($file) as $line) { 52 | $file = basename($file); 53 | 54 | if (preg_match('/^# /', $line)) { 55 | $title = preg_replace('/^# /', '', trim($line)); 56 | $toc[] = "- [$title]({$prefix}{$file})"; 57 | } 58 | } 59 | } 60 | 61 | return implode("\n", [ 62 | '', 63 | '', 64 | ...$toc, 65 | '', 66 | '', 67 | ]); 68 | } 69 | 70 | function updateMarkdown(string $file, string $toc): string 71 | { 72 | $readme = []; 73 | $in_toc = false; 74 | 75 | foreach (file($file) as $line) { 76 | if (preg_match('/BEGIN_TOC/', $line)) { 77 | $in_toc = true; 78 | 79 | continue; 80 | } 81 | 82 | if (preg_match('/END_TOC/', $line)) { 83 | $in_toc = false; 84 | $readme[] = $toc; 85 | 86 | continue; 87 | } 88 | 89 | if ($in_toc) { 90 | continue; 91 | } 92 | 93 | $readme[] = rtrim($line); 94 | } 95 | 96 | return implode("\n", [ 97 | ...$readme, 98 | '', 99 | ]); 100 | } 101 | 102 | // Main README 103 | file_put_contents('./README.md.new', updateMarkdown('./README.md', generateToc(BASE_URL))); 104 | unlink('./README.md'); 105 | rename('./README.md.new', './README.md'); 106 | 107 | // Docs index 108 | file_put_contents('./docs/README.md.new', updateMarkdown('./docs/README.md', generateToc(BASE_URL))); 109 | unlink('./docs/README.md'); 110 | rename('./docs/README.md.new', './docs/README.md'); 111 | 112 | // Page footers 113 | $footer = generateFooter('./'); 114 | foreach (DOC_FILES as $file) { 115 | file_put_contents("./{$file}.new", updateMarkdown("./{$file}", $footer)); 116 | unlink("./{$file}"); 117 | rename("./{$file}.new", "./{$file}"); 118 | } 119 | 120 | echo "\nDONE!\n\n"; 121 | 122 | exit(0); 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev:styles": "npx tailwindcss -i resources/css/internal.css -o resources/dist/filament-peek.css --postcss --watch", 5 | "dev:scripts": "esbuild resources/js/plugin.js --bundle --sourcemap=inline --outfile=resources/dist/filament-peek.js --watch", 6 | "build:styles": "npx tailwindcss -i resources/css/internal.css -o resources/dist/filament-peek.css --postcss --minify && npm run purge", 7 | "build:scripts": "esbuild resources/js/plugin.js --bundle --minify --outfile=resources/dist/filament-peek.js", 8 | "purge": "filament-purge -i resources/dist/filament-peek.css -o resources/dist/filament-peek.css", 9 | "dev": "npm-run-all --parallel dev:*", 10 | "build": "npm-run-all build:*" 11 | }, 12 | "devDependencies": { 13 | "@awcodes/filament-plugin-purge": "^1.1", 14 | "@tailwindcss/forms": "^0.5", 15 | "@tailwindcss/typography": "^0.5", 16 | "alpinejs": "^3.12", 17 | "autoprefixer": "^10.4", 18 | "esbuild": "^0.8", 19 | "lodash.debounce": "^4.0", 20 | "lodash.throttle": "^4.1", 21 | "npm-run-all": "^4.1", 22 | "postcss": "^8.4", 23 | "prettier": "^2.7", 24 | "prettier-plugin-tailwindcss": "^0.1", 25 | "tailwindcss": "^3.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /resources/css/internal.css: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: 3 | * This file is for the internal CSS build distributed with the plugin. 4 | * Use `plugin.css` if you are trying to integrate with a custom Filament Theme. 5 | * 6 | * @see https://github.com/pboivin/filament-peek/blob/2.x/docs/configuration.md#integrating-with-a-custom-theme 7 | */ 8 | 9 | @tailwind utilities; 10 | 11 | /*** Modal structure ***/ 12 | 13 | .filament-peek-modal { 14 | @apply fixed left-0 top-0 z-[9000] h-screen w-screen; 15 | @apply flex bg-gray-50 text-gray-950; 16 | @apply m-0 p-0 !important; 17 | @apply dark:bg-gray-950 dark:text-white; 18 | 19 | --filament-peek-panel-header-height: 4.8rem; 20 | --filament-peek-builder-actions-height: 4.25rem; 21 | } 22 | 23 | .filament-peek-panel { 24 | @apply flex flex-col; 25 | } 26 | 27 | .filament-peek-panel-header { 28 | @apply flex items-center justify-between border-b bg-white p-4; 29 | @apply text-sm font-medium text-gray-900; 30 | @apply dark:border-gray-800 dark:bg-gray-900 dark:text-white; 31 | min-height: var(--filament-peek-panel-header-height); 32 | user-select: none; 33 | } 34 | 35 | .filament-peek-panel-body { 36 | @apply flex-grow bg-gray-100 p-4; 37 | @apply dark:bg-gray-950; 38 | 39 | iframe { 40 | @apply mx-auto shadow-2xl; 41 | transition: all 200ms; 42 | } 43 | 44 | &.allow-iframe-overflow { 45 | @apply overflow-y-auto; 46 | } 47 | } 48 | 49 | body.is-filament-peek-preview-modal-open { 50 | @apply overflow-hidden; 51 | } 52 | 53 | /*** Preview ***/ 54 | 55 | .filament-peek-preview { 56 | @apply flex-grow max-w-full; 57 | 58 | .filament-peek-panel-header > * { 59 | @apply min-w-[10rem]; 60 | } 61 | } 62 | 63 | .filament-peek-device-presets { 64 | @apply hidden justify-center xl:flex; 65 | 66 | button { 67 | @apply mx-2 inline-flex flex-col items-center disabled:opacity-25; 68 | 69 | svg { 70 | @apply h-6 w-6; 71 | } 72 | 73 | &:after { 74 | @apply mt-1 block h-1 w-1 rounded-full bg-transparent; 75 | content: ''; 76 | } 77 | 78 | &.is-active-device-preset:after { 79 | @apply bg-current opacity-25; 80 | } 81 | } 82 | } 83 | 84 | .filament-peek-rotate-preset { 85 | svg { 86 | @apply relative -top-1; 87 | } 88 | } 89 | 90 | .filament-peek-modal-actions { 91 | @apply flex justify-end; 92 | } 93 | 94 | /*** Editor ***/ 95 | 96 | .filament-peek-editor { 97 | @apply hidden border-r rtl:border-l; 98 | @apply dark:border-gray-700; 99 | 100 | .filament-peek-panel-body { 101 | @apply flex h-full w-full p-0; 102 | } 103 | } 104 | 105 | .filament-peek-editor-icon { 106 | @apply gap-0 border-0 p-2 bg-transparent !important; 107 | 108 | &:not(:focus) { 109 | box-shadow: none !important; 110 | } 111 | 112 | svg { 113 | @apply text-gray-900 dark:text-white; 114 | } 115 | 116 | &.is-icon-active { 117 | svg { 118 | @apply text-primary-600; 119 | } 120 | } 121 | } 122 | 123 | .filament-peek-editor-auto-refresh-label { 124 | @apply inline-flex items-center gap-2 p-2; 125 | } 126 | 127 | .filament-peek-builder-editor { 128 | @apply relative flex h-full w-full flex-col; 129 | } 130 | 131 | .filament-peek-builder-content { 132 | @apply w-full overflow-y-auto p-4; 133 | height: calc(100vh - var(--filament-peek-panel-header-height)); 134 | max-height: calc(100vh - var(--filament-peek-panel-header-height)); 135 | 136 | .tippy-content [x-ref="panel"] { 137 | @apply text-gray-900 dark:text-gray-300; 138 | } 139 | } 140 | 141 | .filament-peek-builder-actions { 142 | @apply hidden; 143 | } 144 | 145 | .filament-peek-builder-editor.has-sidebar-actions { 146 | .filament-peek-builder-content { 147 | @apply w-full overflow-y-auto p-4; 148 | height: calc( 149 | 100vh - var(--filament-peek-panel-header-height) - 150 | var(--filament-peek-builder-actions-height) 151 | ); 152 | max-height: calc( 153 | 100vh - var(--filament-peek-panel-header-height) - 154 | var(--filament-peek-builder-actions-height) 155 | ); 156 | } 157 | 158 | .filament-peek-builder-actions { 159 | @apply block w-full border-t dark:border-gray-700; 160 | height: var(--filament-peek-builder-actions-height); 161 | } 162 | 163 | /* Main builder */ 164 | .fi-fo-builder > .fi-fo-builder-block-picker { 165 | @apply absolute bottom-0 left-0 w-full p-4; 166 | } 167 | 168 | /* Nested buidlers */ 169 | .fi-fo-builder .fi-fo-builder > .fi-fo-builder-block-picker { 170 | position: initial; 171 | padding: initial; 172 | } 173 | } 174 | 175 | /*** Resizer ***/ 176 | 177 | .filament-peek-editor-resizer { 178 | @apply absolute left-[100%] top-0 h-full w-[9px] bg-transparent; 179 | @apply border-l-[3px] border-transparent; 180 | @apply rtl:right-[100%] rtl:border-l-0 rtl:border-r-[3px]; 181 | 182 | &:hover { 183 | @apply border-gray-500; 184 | cursor: ew-resize; 185 | } 186 | } 187 | 188 | .filament-peek-iframe-cover { 189 | @apply hidden; 190 | } 191 | 192 | .is-filament-peek-editor-resizing { 193 | user-select: none; 194 | cursor: ew-resize; 195 | 196 | .filament-peek-editor-resizer { 197 | @apply border-gray-500; 198 | } 199 | 200 | .filament-peek-iframe-cover { 201 | @apply block fixed inset-0 z-[9010]; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /resources/css/plugin.css: -------------------------------------------------------------------------------- 1 | /*** Modal structure ***/ 2 | 3 | .filament-peek-modal { 4 | @apply fixed left-0 top-0 z-[9000] h-screen w-screen; 5 | @apply flex bg-gray-50 text-gray-950; 6 | @apply m-0 p-0 !important; 7 | @apply dark:bg-gray-950 dark:text-white; 8 | 9 | --filament-peek-panel-header-height: 4.8rem; 10 | --filament-peek-builder-actions-height: 4.25rem; 11 | } 12 | 13 | .filament-peek-panel { 14 | @apply flex flex-col; 15 | } 16 | 17 | .filament-peek-panel-header { 18 | @apply flex items-center justify-between border-b bg-white p-4; 19 | @apply text-sm font-medium text-gray-900; 20 | @apply dark:border-gray-800 dark:bg-gray-900 dark:text-white; 21 | min-height: var(--filament-peek-panel-header-height); 22 | user-select: none; 23 | } 24 | 25 | .filament-peek-panel-body { 26 | @apply flex-grow bg-gray-100 p-4; 27 | @apply dark:bg-gray-950; 28 | 29 | iframe { 30 | @apply mx-auto shadow-2xl; 31 | transition: all 200ms; 32 | } 33 | 34 | &.allow-iframe-overflow { 35 | @apply overflow-y-auto; 36 | } 37 | } 38 | 39 | body.is-filament-peek-preview-modal-open { 40 | @apply overflow-hidden; 41 | } 42 | 43 | /*** Preview ***/ 44 | 45 | .filament-peek-preview { 46 | @apply flex-grow max-w-full; 47 | 48 | .filament-peek-panel-header > * { 49 | @apply min-w-[10rem]; 50 | } 51 | } 52 | 53 | .filament-peek-device-presets { 54 | @apply hidden justify-center xl:flex; 55 | 56 | button { 57 | @apply mx-2 inline-flex flex-col items-center disabled:opacity-25; 58 | 59 | svg { 60 | @apply h-6 w-6; 61 | } 62 | 63 | &:after { 64 | @apply mt-1 block h-1 w-1 rounded-full bg-transparent; 65 | content: ''; 66 | } 67 | 68 | &.is-active-device-preset:after { 69 | @apply bg-current opacity-25; 70 | } 71 | } 72 | } 73 | 74 | .filament-peek-rotate-preset { 75 | svg { 76 | @apply relative -top-1; 77 | } 78 | } 79 | 80 | .filament-peek-modal-actions { 81 | @apply flex justify-end; 82 | } 83 | 84 | /*** Editor ***/ 85 | 86 | .filament-peek-editor { 87 | @apply hidden border-r rtl:border-l; 88 | @apply dark:border-gray-700; 89 | 90 | .filament-peek-panel-body { 91 | @apply flex h-full w-full p-0; 92 | } 93 | } 94 | 95 | .filament-peek-editor-icon { 96 | @apply gap-0 border-0 p-2 bg-transparent !important; 97 | 98 | &:not(:focus) { 99 | box-shadow: none !important; 100 | } 101 | 102 | svg { 103 | @apply text-gray-900 dark:text-white; 104 | } 105 | 106 | &.is-icon-active { 107 | svg { 108 | @apply text-primary-600; 109 | } 110 | } 111 | } 112 | 113 | .filament-peek-editor-auto-refresh-label { 114 | @apply inline-flex items-center gap-2 p-2; 115 | } 116 | 117 | .filament-peek-builder-editor { 118 | @apply relative flex h-full w-full flex-col; 119 | } 120 | 121 | .filament-peek-builder-content { 122 | @apply w-full overflow-y-auto p-4; 123 | height: calc(100vh - var(--filament-peek-panel-header-height)); 124 | max-height: calc(100vh - var(--filament-peek-panel-header-height)); 125 | 126 | .tippy-content [x-ref="panel"] { 127 | @apply text-gray-900 dark:text-gray-300; 128 | } 129 | } 130 | 131 | .filament-peek-builder-actions { 132 | @apply hidden; 133 | } 134 | 135 | .filament-peek-builder-editor.has-sidebar-actions { 136 | .filament-peek-builder-content { 137 | @apply w-full overflow-y-auto p-4; 138 | height: calc( 139 | 100vh - var(--filament-peek-panel-header-height) - 140 | var(--filament-peek-builder-actions-height) 141 | ); 142 | max-height: calc( 143 | 100vh - var(--filament-peek-panel-header-height) - 144 | var(--filament-peek-builder-actions-height) 145 | ); 146 | } 147 | 148 | .filament-peek-builder-actions { 149 | @apply block w-full border-t dark:border-gray-700; 150 | height: var(--filament-peek-builder-actions-height); 151 | } 152 | 153 | /* Main builder */ 154 | .fi-fo-builder > .fi-fo-builder-block-picker { 155 | @apply absolute bottom-0 left-0 w-full p-4; 156 | } 157 | 158 | /* Nested buidlers */ 159 | .fi-fo-builder .fi-fo-builder > .fi-fo-builder-block-picker { 160 | position: initial; 161 | padding: initial; 162 | } 163 | } 164 | 165 | /*** Resizer ***/ 166 | 167 | .filament-peek-editor-resizer { 168 | @apply absolute left-[100%] top-0 h-full w-[9px] bg-transparent; 169 | @apply border-l-[3px] border-transparent; 170 | @apply rtl:right-[100%] rtl:border-l-0 rtl:border-r-[3px]; 171 | 172 | &:hover { 173 | @apply border-gray-500; 174 | cursor: ew-resize; 175 | } 176 | } 177 | 178 | .filament-peek-iframe-cover { 179 | @apply hidden; 180 | } 181 | 182 | .is-filament-peek-editor-resizing { 183 | user-select: none; 184 | cursor: ew-resize; 185 | 186 | .filament-peek-editor-resizer { 187 | @apply border-gray-500; 188 | } 189 | 190 | .filament-peek-iframe-cover { 191 | @apply block fixed inset-0 z-[9010]; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /resources/dist/filament-peek.css: -------------------------------------------------------------------------------- 1 | .rotate-90{--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.filament-peek-modal{position:fixed;left:0;top:0;z-index:9000;height:100vh;width:100vw;display:flex;background-color:rgba(var(--gray-50),var(--tw-bg-opacity));color:rgba(var(--gray-950),var(--tw-text-opacity));margin:0!important;padding:0!important}.filament-peek-modal,:is(.dark .filament-peek-modal){--tw-bg-opacity:1;--tw-text-opacity:1}:is(.dark .filament-peek-modal){background-color:rgba(var(--gray-950),var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.filament-peek-modal{--filament-peek-panel-header-height:4.8rem;--filament-peek-builder-actions-height:4.25rem}.filament-peek-panel{display:flex;flex-direction:column}.filament-peek-panel-header{display:flex;align-items:center;justify-content:space-between;border-bottom-width:1px;--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));padding:1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity:1;color:rgba(var(--gray-900),var(--tw-text-opacity))}:is(.dark .filament-peek-panel-header){--tw-border-opacity:1;border-color:rgba(var(--gray-800),var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(var(--gray-900),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.filament-peek-panel-header{min-height:var(--filament-peek-panel-header-height);-webkit-user-select:none;-moz-user-select:none;user-select:none}.filament-peek-panel-body{flex-grow:1;--tw-bg-opacity:1;background-color:rgba(var(--gray-100),var(--tw-bg-opacity));padding:1rem}:is(.dark .filament-peek-panel-body){--tw-bg-opacity:1;background-color:rgba(var(--gray-950),var(--tw-bg-opacity))}.filament-peek-panel-body iframe{margin-left:auto;margin-right:auto;--tw-shadow:0 25px 50px -12px #00000040;--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition:all .2s}.filament-peek-panel-body.allow-iframe-overflow{overflow-y:auto}body.is-filament-peek-preview-modal-open{overflow:hidden}.filament-peek-preview{max-width:100%;flex-grow:1}.filament-peek-preview .filament-peek-panel-header>*{min-width:10rem}.filament-peek-device-presets{display:none;justify-content:center}@media (min-width:1280px){.filament-peek-device-presets{display:flex}}.filament-peek-device-presets button{margin-left:.5rem;margin-right:.5rem;display:inline-flex;flex-direction:column;align-items:center}.filament-peek-device-presets button:disabled{opacity:.25}.filament-peek-device-presets button svg{height:1.5rem;width:1.5rem}.filament-peek-device-presets button:after{margin-top:.25rem;display:block;height:.25rem;width:.25rem;border-radius:9999px;background-color:initial;content:""}.filament-peek-device-presets button.is-active-device-preset:after{background-color:currentColor;opacity:.25}.filament-peek-rotate-preset svg{position:relative;top:-.25rem}.filament-peek-modal-actions{display:flex;justify-content:flex-end}.filament-peek-editor{display:none;border-right-width:1px}:is([dir=rtl] .filament-peek-editor){border-left-width:1px}:is(.dark .filament-peek-editor){--tw-border-opacity:1;border-color:rgba(var(--gray-700),var(--tw-border-opacity))}.filament-peek-editor .filament-peek-panel-body{display:flex;height:100%;width:100%;padding:0}.filament-peek-editor-icon{gap:0!important;border-width:0!important;background-color:initial!important;padding:.5rem!important}.filament-peek-editor-icon:not(:focus){box-shadow:none!important}.filament-peek-editor-icon svg{--tw-text-opacity:1;color:rgba(var(--gray-900),var(--tw-text-opacity))}:is(.dark .filament-peek-editor-icon svg){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.filament-peek-editor-icon.is-icon-active svg{--tw-text-opacity:1;color:rgba(var(--primary-600),var(--tw-text-opacity))}.filament-peek-editor-auto-refresh-label{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem}.filament-peek-builder-editor{position:relative;display:flex;height:100%;width:100%;flex-direction:column}.filament-peek-builder-content{width:100%;overflow-y:auto;padding:1rem;height:calc(100vh - var(--filament-peek-panel-header-height));max-height:calc(100vh - var(--filament-peek-panel-header-height))}.filament-peek-builder-content .tippy-content [x-ref=panel]{--tw-text-opacity:1;color:rgba(var(--gray-900),var(--tw-text-opacity))}:is(.dark .filament-peek-builder-content .tippy-content [x-ref=panel]){--tw-text-opacity:1;color:rgba(var(--gray-300),var(--tw-text-opacity))}.filament-peek-builder-actions{display:none}.filament-peek-builder-editor.has-sidebar-actions .filament-peek-builder-content{width:100%;overflow-y:auto;padding:1rem;height:calc(100vh - var(--filament-peek-panel-header-height) - var(--filament-peek-builder-actions-height));max-height:calc(100vh - var(--filament-peek-panel-header-height) - var(--filament-peek-builder-actions-height))}.filament-peek-builder-editor.has-sidebar-actions .filament-peek-builder-actions{display:block;width:100%;border-top-width:1px}:is(.dark .filament-peek-builder-editor.has-sidebar-actions .filament-peek-builder-actions){--tw-border-opacity:1;border-color:rgba(var(--gray-700),var(--tw-border-opacity))}.filament-peek-builder-editor.has-sidebar-actions .filament-peek-builder-actions{height:var(--filament-peek-builder-actions-height)}.filament-peek-builder-editor.has-sidebar-actions .fi-fo-builder>.fi-fo-builder-block-picker{position:absolute;bottom:0;left:0;width:100%;padding:1rem}.filament-peek-builder-editor.has-sidebar-actions .fi-fo-builder .fi-fo-builder>.fi-fo-builder-block-picker{position:static;padding:initial}.filament-peek-editor-resizer{position:absolute;left:100%;top:0;height:100%;width:9px;background-color:initial;border-left-width:3px;border-color:#0000}:is([dir=rtl] .filament-peek-editor-resizer){right:100%;border-left-width:0;border-right-width:3px}.filament-peek-editor-resizer:hover{--tw-border-opacity:1;border-color:rgba(var(--gray-500),var(--tw-border-opacity));cursor:ew-resize}.filament-peek-iframe-cover{display:none}.is-filament-peek-editor-resizing{-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:ew-resize}.is-filament-peek-editor-resizing .filament-peek-editor-resizer{--tw-border-opacity:1;border-color:rgba(var(--gray-500),var(--tw-border-opacity))}.is-filament-peek-editor-resizing .filament-peek-iframe-cover{position:fixed;inset:0;z-index:9010;display:block}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgba(var(--gray-600),var(--tw-border-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgba(var(--gray-700),var(--tw-bg-opacity))}:is(.dark .dark\:text-primary-500){--tw-text-opacity:1;color:rgba(var(--primary-500),var(--tw-text-opacity))}:is(.dark .dark\:checked\:border-primary-600:checked){--tw-border-opacity:1;border-color:rgba(var(--primary-600),var(--tw-border-opacity))}:is(.dark .dark\:checked\:bg-primary-600:checked){--tw-bg-opacity:1;background-color:rgba(var(--primary-600),var(--tw-bg-opacity))} -------------------------------------------------------------------------------- /resources/dist/filament-peek.js: -------------------------------------------------------------------------------- 1 | (()=>{var j=(i,e)=>()=>(e||(e={exports:{}},i(e.exports,e)),e.exports);var M=j((re,R)=>{var D="Expected a function",k=0/0,B="[object Symbol]",_=/^\s+|\s+$/g,F=/^[-+]0x[0-9a-f]+$/i,U=/^0b[01]+$/i,H=/^0o[0-7]+$/i,X=parseInt,K=typeof global=="object"&&global&&global.Object===Object&&global,N=typeof self=="object"&&self&&self.Object===Object&&self,q=K||N||Function("return this")(),$=Object.prototype,G=$.toString,Y=Math.max,J=Math.min,w=function(){return q.Date.now()};function Q(i,e,t){var n,f,p,l,s,d,h=0,E=!1,u=!1,b=!0;if(typeof i!="function")throw new TypeError(D);e=x(e)||0,S(t)&&(E=!!t.leading,u="maxWait"in t,p=u?Y(x(t.maxWait)||0,e):p,b="trailing"in t?!!t.trailing:b);function y(r){var a=n,m=f;return n=f=void 0,h=r,l=i.apply(m,a),l}function C(r){return h=r,s=setTimeout(g,e),E?y(r):l}function L(r){var a=r-d,m=r-h,I=e-a;return u?J(I,p-m):I}function T(r){var a=r-d,m=r-h;return d===void 0||a>=e||a<0||u&&m>=p}function g(){var r=w();if(T(r))return P(r);s=setTimeout(g,L(r))}function P(r){return s=void 0,b&&n?y(r):(n=f=void 0,l)}function z(){s!==void 0&&clearTimeout(s),h=0,n=d=f=s=void 0}function A(){return s===void 0?l:P(w())}function v(){var r=w(),a=T(r);if(n=arguments,f=this,d=r,a){if(s===void 0)return C(d);if(u)return s=setTimeout(g,e),y(d)}return s===void 0&&(s=setTimeout(g,e)),l}return v.cancel=z,v.flush=A,v}function S(i){var e=typeof i;return!!i&&(e=="object"||e=="function")}function V(i){return!!i&&typeof i=="object"}function Z(i){return typeof i=="symbol"||V(i)&&G.call(i)==B}function x(i){if(typeof i=="number")return i;if(Z(i))return k;if(S(i)){var e=typeof i.valueOf=="function"?i.valueOf():i;i=S(e)?e+"":e}if(typeof i!="string")return i===0?i:+i;i=i.replace(_,"");var t=U.test(i);return t||H.test(i)?X(i.slice(2),t?2:8):F.test(i)?k:+i}R.exports=Q});function o(i,e,t={}){i.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0,cancelable:!0}))}var ee=M(),W=[],O={onEditorFocusOut(i){W.push(i)}},c={initialWidth:0,initialX:0};document.addEventListener("alpine:init",()=>{o(document,"peek:initializing"),Alpine.data("PeekPreviewModal",i=>({config:i,isOpen:!1,withEditor:!1,editorHasSidebarActions:!1,editorIsResizable:!1,editorIsResizing:!1,canRotatePreset:!1,activeDevicePreset:null,editorTitle:null,modalTitle:null,iframeUrl:null,iframeContent:null,modalStyle:{display:"none"},iframeStyle:{width:"100%",height:"100%",maxWidth:"100%",maxHeight:"100%"},editorStyle:{display:"none"},init(){o(document,"peek:modal-initializing",{modal:this});let e=this.config.editorAutoRefreshDebounceTime||500,t=this.config.editorSidebarMinWidth||"30rem",n=this.config.editorSidebarInitialWidth||"30rem";this.refreshBuilderPreview=ee(()=>Livewire.dispatch("refreshBuilderPreview"),e),this.editorStyle.width=n,this.config.canResizeEditorSidebar&&(this.editorStyle.minWidth=t,this.editorIsResizable=!0),this.setDevicePreset(),document.documentElement.getAttribute("dir")==="rtl"&&(this.documentIsRtl=!0),setTimeout(()=>o(document,"peek:modal-initialized",{modal:this}),0)},setIframeDimensions(e,t){this.iframeStyle.maxWidth=e,this.iframeStyle.maxHeight=t,this.config.allowIframeOverflow&&(this.iframeStyle.width=e,this.iframeStyle.height=t)},setDevicePreset(e){e=e||this.config.initialDevicePreset,!!this.config.devicePresets&&(!this.config.devicePresets[e]||!this.config.devicePresets[e].width||!this.config.devicePresets[e].height||(this.setIframeDimensions(this.config.devicePresets[e].width,this.config.devicePresets[e].height),this.canRotatePreset=this.config.devicePresets[e].canRotatePreset||!1,this.activeDevicePreset=e))},isActiveDevicePreset(e){return this.activeDevicePreset===e},rotateDevicePreset(){let e=this.iframeStyle.maxHeight,t=this.iframeStyle.maxWidth;this.setIframeDimensions(e,t)},onOpenPreviewTab(e){o(document,"peek:tab-opening",{modal:this}),this.previewTabUrl=e.detail.url,this.previewTab&&this.previewTab.closed===!1?this.previewTab.location=this.previewTabUrl:this.previewTab=window.open(this.previewTabUrl),setTimeout(()=>o(document,"peek:tab-opened",{modal:this}),0)},onOpenPreviewModal(e){o(document,"peek:modal-opening",{modal:this}),document.body.classList.add("is-filament-peek-preview-modal-open"),this.withEditor=!!e.detail.withEditor,this.editorHasSidebarActions=!!e.detail.editorHasSidebarActions,this.editorIsResizing=!1,this.editorTitle=e.detail.editorTitle,this.editorStyle.display=this.withEditor?"flex":"none",this.modalTitle=e.detail.modalTitle,this.iframeUrl=e.detail.iframeUrl,this.iframeContent=e.detail.iframeContent,this.modalStyle.display="flex",this.isOpen=!0,setTimeout(()=>o(document,"peek:modal-opened",{modal:this}),0),setTimeout(()=>this._focusEditorFirstInput(),0),setTimeout(()=>this._attachIframeEscapeKeyListener(),500)},_focusEditorFirstInput(){if(!this.withEditor)return;let e=this.$el.querySelector(".filament-peek-builder-editor input");e&&e.focus()},_attachIframeEscapeKeyListener(){if(!!this.config.shouldCloseModalWithEscapeKey)try{let e=this.$refs.previewModalBody.querySelector("iframe");if(!(e&&e.contentWindow))return;e.contentWindow.addEventListener("keyup",t=>{t.key==="Escape"&&this.handleEscapeKey()})}catch(e){}},onRefreshPreviewModal(e){this.config.shouldRestoreIframePositionOnRefresh&&this._restoreIframeScrollPosition(),this.iframeUrl=e.detail.iframeUrl,this.iframeContent=e.detail.iframeContent},_restoreIframeScrollPosition(){try{let e=this.$refs.previewModalBody.querySelector("iframe");e&&e.contentWindow&&(this._iframeScrollPosition=e.contentWindow.scrollY,e.onload=()=>{e?.contentWindow?.scrollTo(0,this._iframeScrollPosition||0)})}catch(e){}},onClosePreviewModal(e){setTimeout(()=>this._closeModal(),e?.detail?.delay?250:0)},_closeModal(){o(document,"peek:modal-closing",{modal:this}),document.body.classList.remove("is-filament-peek-preview-modal-open"),this.withEditor=!1,this.editorHasSidebarActions=!1,this.editorIsResizing=!1,this.editorStyle.display="none",this.editorTitle=null,this.modalStyle.display="none",this.modalTitle=null,this.iframeUrl=null,this.iframeContent=null,this.isOpen=!1,setTimeout(()=>o(document,"peek:modal-closed",{modal:this}),0)},onEditorFocusOut(e){if(!!this.editorShouldAutoRefresh()&&this.getAutoRefreshStrategy()!=="reactive")for(let t of W)typeof t=="function"&&t(e,this)},editorShouldAutoRefresh(){if(!!this.withEditor&&!!this.$refs.builderEditor)return!!this.$refs.builderEditor.dataset.shouldAutoRefresh},getAutoRefreshStrategy(){if(!!this.withEditor&&!!this.$refs.builderEditor)return this.$refs.builderEditor.dataset.autoRefreshStrategy||"simple"},handleEscapeKey(){!this.isOpen||!this.config.shouldCloseModalWithEscapeKey||this.withEditor||this.onClosePreviewModal()},acceptEditorChanges(){Livewire.dispatch("closeBuilderEditor")},discardEditorChanges(){this.$dispatch("close-preview-modal"),Livewire.dispatch("resetBuilderEditor")},closePreviewModal(){this.$dispatch("close-preview-modal")},onEditorResizerMouseDown(e){!this.$refs.builderEditor||(this.editorIsResizing=!0,c.initialWidth=parseFloat(getComputedStyle(this.$refs.builderEditor).width),c.initialX=e.clientX)},onMouseUp(e){!this.editorIsResizing||(this.editorIsResizing=!1)},onMouseMove(e){!this.editorIsResizing||(this.documentIsRtl?this.editorStyle.width=c.initialWidth-(e.clientX-c.initialX)+"px":this.editorStyle.width=c.initialWidth+(e.clientX-c.initialX)+"px")}})),o(document,"peek:initialized")});document.addEventListener("peek:initializing",()=>{O.onEditorFocusOut((i,e)=>{if(["input","select","textarea","trix-editor","hex-color-picker"].includes(i.target.tagName.toLowerCase())){e.refreshBuilderPreview();return}if(i.target.tagName.toLowerCase()==="button"&&i.target.getAttribute("role")==="switch"){e.refreshBuilderPreview();return}if(i.target.classList.contains("ProseMirror")){e.refreshBuilderPreview();return}})});window.FilamentPeek=O;})(); 2 | -------------------------------------------------------------------------------- /resources/js/plugin.js: -------------------------------------------------------------------------------- 1 | const debounce = require('lodash.debounce'); 2 | 3 | import { dispatch } from 'alpinejs/src/utils/dispatch'; 4 | 5 | const editorFocusOutHandlers = []; 6 | 7 | const Peek = { 8 | onEditorFocusOut(callback) { 9 | editorFocusOutHandlers.push(callback); 10 | }, 11 | }; 12 | 13 | const resizerState = { 14 | initialWidth: 0, 15 | initialX: 0, 16 | }; 17 | 18 | document.addEventListener('alpine:init', () => { 19 | dispatch(document, 'peek:initializing'); 20 | 21 | Alpine.data('PeekPreviewModal', (config) => ({ 22 | config, 23 | isOpen: false, 24 | withEditor: false, 25 | editorHasSidebarActions: false, 26 | editorIsResizable: false, 27 | editorIsResizing: false, 28 | canRotatePreset: false, 29 | activeDevicePreset: null, 30 | editorTitle: null, 31 | modalTitle: null, 32 | iframeUrl: null, 33 | iframeContent: null, 34 | modalStyle: { 35 | display: 'none', 36 | }, 37 | iframeStyle: { 38 | width: '100%', 39 | height: '100%', 40 | maxWidth: '100%', 41 | maxHeight: '100%', 42 | }, 43 | editorStyle: { 44 | display: 'none', 45 | }, 46 | 47 | init() { 48 | dispatch(document, 'peek:modal-initializing', { modal: this }); 49 | 50 | const debounceTime = this.config.editorAutoRefreshDebounceTime || 500; 51 | const editorSidebarMinWidth = this.config.editorSidebarMinWidth || '30rem'; 52 | const editorSidebarInitialWidth = this.config.editorSidebarInitialWidth || '30rem'; 53 | 54 | this.refreshBuilderPreview = debounce(() => Livewire.dispatch('refreshBuilderPreview'), debounceTime); 55 | 56 | this.editorStyle.width = editorSidebarInitialWidth; 57 | 58 | if (this.config.canResizeEditorSidebar) { 59 | this.editorStyle.minWidth = editorSidebarMinWidth; 60 | this.editorIsResizable = true; 61 | } 62 | 63 | this.setDevicePreset(); 64 | 65 | if (document.documentElement.getAttribute('dir') === 'rtl') { 66 | this.documentIsRtl = true; 67 | } 68 | 69 | setTimeout(() => dispatch(document, 'peek:modal-initialized', { modal: this }), 0); 70 | }, 71 | 72 | setIframeDimensions(width, height) { 73 | this.iframeStyle.maxWidth = width; 74 | this.iframeStyle.maxHeight = height; 75 | 76 | if (this.config.allowIframeOverflow) { 77 | this.iframeStyle.width = width; 78 | this.iframeStyle.height = height; 79 | } 80 | }, 81 | 82 | setDevicePreset(name) { 83 | name = name || this.config.initialDevicePreset; 84 | 85 | if (!this.config.devicePresets) return; 86 | if (!this.config.devicePresets[name]) return; 87 | if (!this.config.devicePresets[name].width) return; 88 | if (!this.config.devicePresets[name].height) return; 89 | 90 | this.setIframeDimensions(this.config.devicePresets[name].width, this.config.devicePresets[name].height); 91 | 92 | this.canRotatePreset = this.config.devicePresets[name].canRotatePreset || false; 93 | 94 | this.activeDevicePreset = name; 95 | }, 96 | 97 | isActiveDevicePreset(name) { 98 | return this.activeDevicePreset === name; 99 | }, 100 | 101 | rotateDevicePreset() { 102 | const newMaxWidth = this.iframeStyle.maxHeight; 103 | const newMaxHeight = this.iframeStyle.maxWidth; 104 | 105 | this.setIframeDimensions(newMaxWidth, newMaxHeight); 106 | }, 107 | 108 | onOpenPreviewTab($event) { 109 | dispatch(document, 'peek:tab-opening', { modal: this }); 110 | 111 | this.previewTabUrl = $event.detail.url; 112 | 113 | if (this.previewTab && this.previewTab.closed === false) { 114 | this.previewTab.location = this.previewTabUrl; 115 | } else { 116 | this.previewTab = window.open(this.previewTabUrl); 117 | } 118 | 119 | setTimeout(() => dispatch(document, 'peek:tab-opened', { modal: this }), 0); 120 | }, 121 | 122 | onOpenPreviewModal($event) { 123 | dispatch(document, 'peek:modal-opening', { modal: this }); 124 | 125 | document.body.classList.add('is-filament-peek-preview-modal-open'); 126 | 127 | this.withEditor = !!$event.detail.withEditor; 128 | this.editorHasSidebarActions = !!$event.detail.editorHasSidebarActions; 129 | this.editorIsResizing = false; 130 | this.editorTitle = $event.detail.editorTitle; 131 | this.editorStyle.display = this.withEditor ? 'flex' : 'none'; 132 | this.modalTitle = $event.detail.modalTitle; 133 | this.iframeUrl = $event.detail.iframeUrl; 134 | this.iframeContent = $event.detail.iframeContent; 135 | this.modalStyle.display = 'flex'; 136 | this.isOpen = true; 137 | 138 | setTimeout(() => dispatch(document, 'peek:modal-opened', { modal: this }), 0); 139 | 140 | setTimeout(() => this._focusEditorFirstInput(), 0); 141 | 142 | setTimeout(() => this._attachIframeEscapeKeyListener(), 500); 143 | }, 144 | 145 | _focusEditorFirstInput() { 146 | if (!this.withEditor) return; 147 | 148 | const firstInput = this.$el.querySelector('.filament-peek-builder-editor input'); 149 | 150 | firstInput && firstInput.focus(); 151 | }, 152 | 153 | _attachIframeEscapeKeyListener() { 154 | if (!this.config.shouldCloseModalWithEscapeKey) return; 155 | 156 | try { 157 | const iframe = this.$refs.previewModalBody.querySelector('iframe'); 158 | 159 | if (!(iframe && iframe.contentWindow)) return; 160 | 161 | iframe.contentWindow.addEventListener('keyup', (e) => { 162 | if (e.key === 'Escape') this.handleEscapeKey(); 163 | }); 164 | } catch (e) { 165 | // pass 166 | } 167 | }, 168 | 169 | onRefreshPreviewModal($event) { 170 | if (this.config.shouldRestoreIframePositionOnRefresh) { 171 | this._restoreIframeScrollPosition(); 172 | } 173 | 174 | this.iframeUrl = $event.detail.iframeUrl; 175 | this.iframeContent = $event.detail.iframeContent; 176 | }, 177 | 178 | _restoreIframeScrollPosition() { 179 | try { 180 | const iframe = this.$refs.previewModalBody.querySelector('iframe'); 181 | 182 | if (iframe && iframe.contentWindow) { 183 | this._iframeScrollPosition = iframe.contentWindow.scrollY; 184 | iframe.onload = () => { 185 | iframe?.contentWindow?.scrollTo(0, this._iframeScrollPosition || 0); 186 | } 187 | } 188 | } catch (e) { 189 | // pass 190 | } 191 | }, 192 | 193 | onClosePreviewModal($event) { 194 | setTimeout(() => this._closeModal(), $event?.detail?.delay ? 250 : 0); 195 | }, 196 | 197 | _closeModal() { 198 | dispatch(document, 'peek:modal-closing', { modal: this }); 199 | 200 | document.body.classList.remove('is-filament-peek-preview-modal-open'); 201 | 202 | this.withEditor = false; 203 | this.editorHasSidebarActions = false; 204 | this.editorIsResizing = false; 205 | this.editorStyle.display = 'none'; 206 | this.editorTitle = null; 207 | this.modalStyle.display = 'none'; 208 | this.modalTitle = null; 209 | this.iframeUrl = null; 210 | this.iframeContent = null; 211 | this.isOpen = false; 212 | 213 | setTimeout(() => dispatch(document, 'peek:modal-closed', { modal: this }), 0); 214 | }, 215 | 216 | onEditorFocusOut($event) { 217 | if (!this.editorShouldAutoRefresh()) return; 218 | if (this.getAutoRefreshStrategy() === 'reactive') return; 219 | 220 | for (let handler of editorFocusOutHandlers) { 221 | if (typeof handler === 'function') { 222 | handler($event, this); 223 | } 224 | } 225 | }, 226 | 227 | editorShouldAutoRefresh() { 228 | if (!this.withEditor) return; 229 | if (!this.$refs.builderEditor) return; 230 | 231 | return !!this.$refs.builderEditor.dataset.shouldAutoRefresh; 232 | }, 233 | 234 | getAutoRefreshStrategy() { 235 | if (!this.withEditor) return; 236 | if (!this.$refs.builderEditor) return; 237 | 238 | return this.$refs.builderEditor.dataset.autoRefreshStrategy || 'simple'; 239 | }, 240 | 241 | handleEscapeKey() { 242 | if (!this.isOpen) return; 243 | if (!this.config.shouldCloseModalWithEscapeKey) return; 244 | if (this.withEditor) return; 245 | 246 | this.onClosePreviewModal(); 247 | }, 248 | 249 | acceptEditorChanges() { 250 | Livewire.dispatch('closeBuilderEditor'); 251 | }, 252 | 253 | discardEditorChanges() { 254 | this.$dispatch('close-preview-modal'); 255 | 256 | Livewire.dispatch('resetBuilderEditor'); 257 | }, 258 | 259 | closePreviewModal() { 260 | this.$dispatch('close-preview-modal'); 261 | }, 262 | 263 | onEditorResizerMouseDown($event) { 264 | if (!this.$refs.builderEditor) return; 265 | 266 | this.editorIsResizing = true; 267 | 268 | resizerState.initialWidth = parseFloat(getComputedStyle(this.$refs.builderEditor).width); 269 | resizerState.initialX = $event.clientX; 270 | }, 271 | 272 | onMouseUp($event) { 273 | if (!this.editorIsResizing) return; 274 | 275 | this.editorIsResizing = false; 276 | }, 277 | 278 | onMouseMove($event) { 279 | if (!this.editorIsResizing) return; 280 | 281 | if (this.documentIsRtl) { 282 | this.editorStyle.width = (resizerState.initialWidth - ($event.clientX - resizerState.initialX)) + 'px'; 283 | } else { 284 | this.editorStyle.width = (resizerState.initialWidth + ($event.clientX - resizerState.initialX)) + 'px'; 285 | } 286 | }, 287 | })); 288 | 289 | dispatch(document, 'peek:initialized'); 290 | }); 291 | 292 | document.addEventListener('peek:initializing', () => { 293 | Peek.onEditorFocusOut(($event, $modal) => { 294 | // built-in field tags 295 | const autorefreshTags = [ 296 | 'input', 297 | 'select', 298 | 'textarea', 299 | 'trix-editor', 300 | 'hex-color-picker', 301 | ]; 302 | 303 | if (autorefreshTags.includes($event.target.tagName.toLowerCase())) { 304 | $modal.refreshBuilderPreview(); 305 | return; 306 | } 307 | 308 | // built-in toggle field 309 | if ( 310 | $event.target.tagName.toLowerCase() === 'button' && 311 | $event.target.getAttribute('role') === 'switch' 312 | ) { 313 | $modal.refreshBuilderPreview(); 314 | return; 315 | } 316 | 317 | // filament-tiptap-editor 318 | if ($event.target.classList.contains('ProseMirror')) { 319 | $modal.refreshBuilderPreview(); 320 | return; 321 | } 322 | }); 323 | }); 324 | 325 | window.FilamentPeek = Peek; 326 | -------------------------------------------------------------------------------- /resources/lang/ar/ui.php: -------------------------------------------------------------------------------- 1 | 'معاينة', 5 | 6 | 'preview-modal-title' => 'المعاينة', 7 | 8 | 'close-modal-action-label' => 'اغلاق', 9 | 10 | 'builder-editor-title' => 'المحرر', 11 | 12 | 'refresh-action-label' => 'تحديث المعاينة', 13 | 14 | 'editor-settings-label' => 'اعدادات', 15 | 16 | 'editor-auto-refresh-label' => 'تحديث تلقائي', 17 | 18 | 'accept-action-label' => 'حفظ', 19 | 20 | 'discard-action-label' => 'إلغاء', 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/lang/cs/ui.php: -------------------------------------------------------------------------------- 1 | 'Náhled', 5 | 6 | 'preview-modal-title' => 'Náhled', 7 | 8 | 'close-modal-action-label' => 'Zavřít', 9 | 10 | 'builder-editor-title' => 'Editor', 11 | 12 | 'refresh-action-label' => 'Obnovit náhled', 13 | 14 | 'editor-settings-label' => 'Nastavení', 15 | 16 | 'editor-auto-refresh-label' => 'Automatické obnovení', 17 | 18 | 'accept-action-label' => 'Přijmout', 19 | 20 | 'discard-action-label' => 'Zrušit', 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/lang/en/ui.php: -------------------------------------------------------------------------------- 1 | 'Preview', 5 | 6 | 'preview-modal-title' => 'Preview', 7 | 8 | 'close-modal-action-label' => 'Close', 9 | 10 | 'builder-editor-title' => 'Editor', 11 | 12 | 'refresh-action-label' => 'Refresh preview', 13 | 14 | 'editor-settings-label' => 'Settings', 15 | 16 | 'editor-auto-refresh-label' => 'Refresh automatically', 17 | 18 | 'accept-action-label' => 'Accept', 19 | 20 | 'discard-action-label' => 'Discard', 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/lang/es/ui.php: -------------------------------------------------------------------------------- 1 | 'Vista previa', 5 | 6 | 'preview-modal-title' => 'Vista previa', 7 | 8 | 'close-modal-action-label' => 'Cerrar', 9 | 10 | 'builder-editor-title' => 'Editor', 11 | 12 | 'refresh-action-label' => 'Actualizar vista previa', 13 | 14 | 'editor-settings-label' => 'Configuración', 15 | 16 | 'editor-auto-refresh-label' => 'Actualizar automáticamente', 17 | 18 | 'accept-action-label' => 'Aceptar', 19 | 20 | 'discard-action-label' => 'Cancelar', 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/lang/fr/ui.php: -------------------------------------------------------------------------------- 1 | 'Prévisualiser', 5 | 6 | 'preview-modal-title' => 'Prévisualisation', 7 | 8 | 'close-modal-action-label' => 'Fermer', 9 | 10 | 'builder-editor-title' => 'Éditeur', 11 | 12 | 'refresh-action-label' => 'Rafraîchir la prévisualisation', 13 | 14 | 'editor-settings-label' => 'Réglages', 15 | 16 | 'editor-auto-refresh-label' => 'Rafraîchir automatiquement', 17 | 18 | 'accept-action-label' => 'Accepter', 19 | 20 | 'discard-action-label' => 'Annuler', 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/lang/nl/ui.php: -------------------------------------------------------------------------------- 1 | 'Preview', 5 | 6 | 'preview-modal-title' => 'Preview', 7 | 8 | 'close-modal-action-label' => 'Sluiten', 9 | 10 | 'builder-editor-title' => 'Editor', 11 | 12 | 'refresh-action-label' => 'Preview verversen', 13 | 14 | 'editor-settings-label' => 'Instellingen', 15 | 16 | 'editor-auto-refresh-label' => 'Automatisch verversen', 17 | 18 | 'accept-action-label' => 'Accepteren', 19 | 20 | 'discard-action-label' => 'Weggooien', 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/lang/tr/ui.php: -------------------------------------------------------------------------------- 1 | 'Önizleme', 5 | 6 | 'preview-modal-title' => 'Önizleme', 7 | 8 | 'close-modal-action-label' => 'Kapat', 9 | 10 | 'builder-editor-title' => 'Editör', 11 | 12 | 'refresh-action-label' => 'Önizlemeyi Yenile', 13 | 14 | 'editor-settings-label' => 'Ayarlar', 15 | 16 | 'editor-auto-refresh-label' => 'Otomatik Yenile', 17 | 18 | 'accept-action-label' => 'Kabul Et', 19 | 20 | 'discard-action-label' => 'Vazgeç', 21 | ]; 22 | -------------------------------------------------------------------------------- /resources/views/components/preview-link.blade.php: -------------------------------------------------------------------------------- 1 | @if ($alignmentClass = $getAlignmentClass()) 2 |
3 | @endif 4 | 5 | @if ($isButton()) 6 | merge($getExtraAttributes()) }} 12 | > 13 | {{ $getLabel() }} 14 | 15 | @else 16 | class(['text-primary-600', 'dark:text-primary-500', $getUnderlineClass()]) 20 | ->merge($getExtraAttributes()) 21 | }} 22 | > 23 | {{ $getLabel() }} 24 | 25 | @endif 26 | 27 | @if ($getAlignmentClass()) 28 |
29 | @endif 30 | -------------------------------------------------------------------------------- /resources/views/livewire/builder-editor.blade.php: -------------------------------------------------------------------------------- 1 |
canAutoRefresh()) data-auto-refresh-strategy="{{ $this->autoRefreshStrategy }}" @endif 6 | @if ($this->shouldAutoRefresh()) data-should-auto-refresh="1" @endif 7 | > 8 |
9 |
10 | 11 |
12 | 24 | 25 | @if ($this->canAutoRefresh()) 26 | 30 | 31 | 38 | 39 | 40 | 41 | 53 | 54 | 55 | @endif 56 |
57 |
58 | 59 |
63 |
69 |
70 |
71 | {{ $this->form }} 72 | 73 | 76 |
77 | 78 | 79 |
80 | 81 |
82 | 83 |
88 |
89 |
90 |
91 | -------------------------------------------------------------------------------- /resources/views/partials/icon-rotate.blade.php: -------------------------------------------------------------------------------- 1 | {{-- Adapted from Heroicons v1 (https://v1.heroicons.com/) --}} 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/views/partials/modal-actions.blade.php: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /resources/views/preview-modal.blade.php: -------------------------------------------------------------------------------- 1 | @if (\Pboivin\FilamentPeek\Support\View::needsPreviewModal()) 2 |
32 | @if (\Pboivin\FilamentPeek\Support\View::needsBuilderEditor()) 33 | @livewire('filament-peek::builder-editor') 34 | @endif 35 | 36 |
37 |
38 |
43 | 44 | @if (config('filament-peek.devicePresets', false)) 45 |
46 | @foreach (config('filament-peek.devicePresets') as $presetName => $presetConfig) 47 | 58 | @endforeach 59 | 60 | 68 |
69 | @endif 70 | 71 |
72 | @include('filament-peek::partials.modal-actions') 73 |
74 |
75 | 76 |
83 | 90 | 91 | 98 | 99 |
100 |
101 |
102 |
103 | @endif 104 | -------------------------------------------------------------------------------- /routes/preview.php: -------------------------------------------------------------------------------- 1 | middleware(config('filament-peek.internalPreviewUrl.middleware', [])) 10 | ->group(function () { 11 | Route::get('preview', function () { 12 | abort_unless($token = Request::query('token'), 404); 13 | 14 | abort_unless($preview = CachedPreview::get($token), 404); 15 | 16 | if (Request::wantsJson()) { 17 | return $preview->data; 18 | } 19 | 20 | return $preview->render(); 21 | })->name('filament-peek.preview'); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/CachedBuilderPreview.php: -------------------------------------------------------------------------------- 1 | pageClass::renderBuilderPreview($this->view, $this->data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/CachedPreview.php: -------------------------------------------------------------------------------- 1 | pageClass::renderPreviewModalView($this->view, $this->data); 28 | } 29 | 30 | public function put(string $token, int $ttl = 60): bool 31 | { 32 | return Cache::store(static::$cacheStore)->put("filament-peek-preview-{$token}", $this, $ttl); 33 | } 34 | 35 | public static function get(string $token): ?CachedPreview 36 | { 37 | return Cache::store(static::$cacheStore)->get("filament-peek-preview-{$token}"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Exceptions/PreviewModalException.php: -------------------------------------------------------------------------------- 1 | shouldLoadPluginScripts = false; 28 | 29 | return $this; 30 | } 31 | 32 | public function disablePluginStyles(): self 33 | { 34 | $this->shouldLoadPluginStyles = false; 35 | 36 | return $this; 37 | } 38 | 39 | public function shouldLoadPluginScripts(): bool 40 | { 41 | return $this->shouldLoadPluginScripts; 42 | } 43 | 44 | public function shouldLoadPluginStyles(): bool 45 | { 46 | return $this->shouldLoadPluginStyles; 47 | } 48 | 49 | public static function make(): static 50 | { 51 | return app(static::class); 52 | } 53 | 54 | public function getId(): string 55 | { 56 | return static::ID; 57 | } 58 | 59 | public function register(Panel $panel): void 60 | { 61 | Livewire::component( 62 | 'filament-peek::builder-editor', 63 | config('filament-peek.builderEditor.livewireComponentClass', BuilderEditor::class) 64 | ); 65 | 66 | $panel->renderHook( 67 | 'panels::body.end', 68 | fn () => view('filament-peek::preview-modal'), 69 | ); 70 | 71 | if ($this->shouldLoadPluginScripts()) { 72 | FilamentAsset::register([ 73 | Js::make(static::ID, __DIR__.'/../resources/dist/filament-peek.js'), 74 | ], package: static::PACKAGE); 75 | } 76 | 77 | if ($this->shouldLoadPluginStyles()) { 78 | FilamentAsset::register([ 79 | Css::make(static::ID, __DIR__.'/../resources/dist/filament-peek.css'), 80 | ], package: static::PACKAGE); 81 | } 82 | } 83 | 84 | public function boot(Panel $panel): void 85 | { 86 | // 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/FilamentPeekServiceProvider.php: -------------------------------------------------------------------------------- 1 | name(FilamentPeekPlugin::ID) 13 | ->hasTranslations() 14 | ->hasConfigFile() 15 | ->hasViews() 16 | ->hasRoute('preview'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Forms/Actions/InlinePreviewAction.php: -------------------------------------------------------------------------------- 1 | label(__('filament-peek::ui.preview-action-label')) 26 | ->link() 27 | ->action(function ($livewire) { 28 | Support\Panel::ensurePluginIsLoaded(); 29 | 30 | Support\Page::ensurePreviewModalSupport($livewire); 31 | 32 | if ($this->builderField) { 33 | Support\Page::ensureBuilderPreviewSupport($livewire); 34 | 35 | $livewire->openPreviewModalForBuidler($this->builderField); 36 | } else { 37 | $livewire->initialPreviewModalData( 38 | $this->evaluate($this->previewModalData) 39 | ); 40 | 41 | $livewire->openPreviewModal(); 42 | } 43 | }); 44 | 45 | Support\View::setupPreviewModal(); 46 | } 47 | 48 | public function builderPreview(string $builderField = 'blocks'): static 49 | { 50 | Support\View::setupBuilderEditor(); 51 | 52 | $this->builderField = $builderField; 53 | 54 | return $this; 55 | } 56 | 57 | /** Alias for builderPreview */ 58 | public function builderName(string $builderField = 'blocks'): static 59 | { 60 | $this->builderPreview($builderField); 61 | 62 | return $this; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Forms/Components/PreviewLink.php: -------------------------------------------------------------------------------- 1 | configure() 30 | ->label(__('filament-peek::ui.preview-action-label')); 31 | } 32 | 33 | public function getLivewire(): HasForms 34 | { 35 | $livewire = parent::getLivewire(); 36 | 37 | if ($this->builderField) { 38 | Support\Panel::ensurePluginIsLoaded(); 39 | 40 | Support\Page::ensureBuilderPreviewSupport($livewire); 41 | } 42 | 43 | return $livewire; 44 | } 45 | 46 | public function builderPreview(string $value = 'blocks'): static 47 | { 48 | Support\View::setupBuilderEditor(); 49 | 50 | $this->builderField = $value; 51 | 52 | return $this; 53 | } 54 | 55 | public function getBuilderField(): ?string 56 | { 57 | return $this->builderField; 58 | } 59 | 60 | public function alignLeft(): static 61 | { 62 | $this->alignment = 'left'; 63 | 64 | return $this; 65 | } 66 | 67 | public function alignCenter(): static 68 | { 69 | $this->alignment = 'center'; 70 | 71 | return $this; 72 | } 73 | 74 | public function alignRight(): static 75 | { 76 | $this->alignment = 'right'; 77 | 78 | return $this; 79 | } 80 | 81 | public function getAlignment(): ?string 82 | { 83 | return $this->alignment; 84 | } 85 | 86 | public function getAlignmentClass(): ?string 87 | { 88 | return match ($this->alignment) { 89 | 'left' => 'flex justify-start', 90 | 'center' => 'flex justify-center', 91 | 'right' => 'flex justify-end', 92 | default => null, 93 | }; 94 | } 95 | 96 | public function underline(bool $value = true): static 97 | { 98 | $this->isUnderlined = $value; 99 | 100 | return $this; 101 | } 102 | 103 | public function getUnderlineClass(): string 104 | { 105 | return $this->isUnderlined ? 'underline' : ''; 106 | } 107 | 108 | public function getPreviewAction(): ?string 109 | { 110 | if ($this->builderField) { 111 | return "openPreviewModalForBuidler('{$this->builderField}')"; 112 | } 113 | 114 | return 'openPreviewModal'; 115 | } 116 | 117 | public function button(bool $value = true): static 118 | { 119 | $this->isButton = $value; 120 | 121 | return $this; 122 | } 123 | 124 | public function isButton(): bool 125 | { 126 | return $this->isButton; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Livewire/BuilderEditor.php: -------------------------------------------------------------------------------- 1 | autoRefreshStrategy = config('filament-peek.builderEditor.autoRefreshStrategy', 'simple'); 53 | 54 | if ($this->canAutoRefresh()) { 55 | $this->autoRefresh = (bool) session('peek_builder_editor_auto_refresh'); 56 | } 57 | } 58 | 59 | public function render(): ViewContract 60 | { 61 | if ($this->shouldAutoRefresh()) { 62 | try { 63 | $this->refreshBuilderPreview(); 64 | } catch (ValidationException $e) { 65 | // pass 66 | } 67 | } 68 | 69 | return view('filament-peek::livewire.builder-editor'); 70 | } 71 | 72 | public function canAutoRefresh(): bool 73 | { 74 | return (bool) config('filament-peek.builderEditor.canEnableAutoRefresh', false); 75 | } 76 | 77 | public function shouldAutoRefresh(): bool 78 | { 79 | return $this->canAutoRefresh() && $this->autoRefresh; 80 | } 81 | 82 | public function updatedAutoRefresh($value): void 83 | { 84 | session()->put('peek_builder_editor_auto_refresh', (bool) $value); 85 | } 86 | 87 | public function updatedEditorData(): void 88 | { 89 | if ($this->shouldAutoRefresh()) { 90 | $this->refreshBuilderPreview(); 91 | } 92 | } 93 | 94 | public function openBuilderEditor( 95 | array $editorData, 96 | string $builderName, 97 | string $pageClass, 98 | ?string $previewView, 99 | ?string $previewUrl, 100 | ?string $modalTitle, 101 | ?string $editorTitle, 102 | ): void { 103 | $this->previewUrl = $previewUrl; 104 | $this->previewView = $previewView; 105 | $this->pageClass = $pageClass; 106 | $this->builderName = $builderName; 107 | 108 | $this->form->fill($editorData); 109 | 110 | $this->dispatch( 111 | 'open-preview-modal', 112 | modalTitle: $modalTitle ?: '', 113 | editorTitle: $editorTitle ?: '', 114 | withEditor: true, 115 | editorHasSidebarActions: $this->pageClass::builderEditorHasSidebarActions($this->builderName), 116 | ); 117 | 118 | $this->refreshBuilderPreview(); 119 | } 120 | 121 | public function refreshBuilderPreview(): void 122 | { 123 | if (! $this->pageClass || ! $this->builderName) { 124 | return; 125 | } 126 | 127 | if (! $this->previewUrl && ! $this->previewView) { 128 | throw new InvalidArgumentException('Missing preview modal URL or Blade view.'); 129 | } 130 | 131 | if ($this->refreshRequested) { 132 | return; 133 | } 134 | 135 | $this->refreshRequested = true; 136 | 137 | $this->dispatch( 138 | 'refresh-preview-modal', 139 | iframeUrl: $this->getPreviewModalUrl(), 140 | iframeContent: $this->getPreviewModalHtmlContent(), 141 | ); 142 | 143 | $this->refreshCount++; 144 | } 145 | 146 | public function closeBuilderEditor(): void 147 | { 148 | // Trigger validation 149 | $this->form->getState(); 150 | 151 | $this->dispatch( 152 | 'updateBuilderFieldWithEditorData', 153 | builderName: $this->builderName, 154 | editorData: $this->editorData, 155 | ); 156 | 157 | $this->dispatch( 158 | 'close-preview-modal', 159 | delay: true, 160 | ); 161 | 162 | $this->dispatch('resetBuilderEditor')->self(); 163 | } 164 | 165 | public function resetBuilderEditor(): void 166 | { 167 | $this->previewUrl = null; 168 | $this->previewView = null; 169 | $this->builderName = null; 170 | $this->pageClass = null; 171 | } 172 | 173 | public function submit(): void 174 | { 175 | $this->refreshBuilderPreview(); 176 | } 177 | 178 | protected function getFormSchema(): array 179 | { 180 | if (! $this->pageClass || ! $this->builderName) { 181 | return []; 182 | } 183 | 184 | if ($schema = $this->pageClass::getBuilderEditorSchema($this->builderName)) { 185 | return Arr::wrap($schema); 186 | } 187 | 188 | throw new InvalidArgumentException('Missing Builder editor schema.'); 189 | } 190 | 191 | protected function getFormStatePath(): ?string 192 | { 193 | return 'editorData'; 194 | } 195 | 196 | protected function getPreviewData(): array 197 | { 198 | if (! $this->pageClass || ! $this->builderName) { 199 | return []; 200 | } 201 | 202 | if (! $this->previewData) { 203 | $formState = $this->form->getState(); 204 | 205 | $this->previewData = $this->pageClass::mutateBuilderPreviewData( 206 | $this->builderName, 207 | $this->editorData, 208 | $this->pageClass::prepareBuilderPreviewData($formState), 209 | ); 210 | } 211 | 212 | return $this->previewData; 213 | } 214 | 215 | protected function getPreviewModalUrl(): ?string 216 | { 217 | if ($this->previewUrl) { 218 | return $this->previewUrl; 219 | } 220 | 221 | if ($this->previewView && $this->shouldUseInternalPreviewUrl()) { 222 | $token = app(Support\Cache::class)->createPreviewToken(); 223 | 224 | CachedBuilderPreview::make($this->pageClass, $this->previewView, $this->getPreviewData()) 225 | ->put($token, config('filament-peek.internalPreviewUrl.cacheDuration', 60)); 226 | 227 | return route('filament-peek.preview', [ 228 | 'token' => $token, 229 | 'refresh' => $this->refreshCount, 230 | ]); 231 | } 232 | 233 | return null; 234 | } 235 | 236 | protected function getPreviewModalHtmlContent(): ?string 237 | { 238 | if ($this->previewUrl) { 239 | return null; 240 | } 241 | 242 | if ($this->shouldUseInternalPreviewUrl()) { 243 | return null; 244 | } 245 | 246 | if ($this->previewView) { 247 | return $this->pageClass::renderBuilderPreview( 248 | $this->previewView, 249 | $this->getPreviewData(), 250 | ); 251 | } 252 | 253 | return null; 254 | } 255 | 256 | protected function shouldUseInternalPreviewUrl() 257 | { 258 | return config('filament-peek.builderEditor.useInternalPreviewUrl', true) 259 | && config('filament-peek.internalPreviewUrl.enabled', false); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/Pages/Actions/PreviewAction.php: -------------------------------------------------------------------------------- 1 | label(__('filament-peek::ui.preview-action-label')) 23 | ->color('gray') 24 | ->action(function ($livewire) { 25 | Support\Panel::ensurePluginIsLoaded(); 26 | 27 | Support\Page::ensurePreviewModalSupport($livewire); 28 | 29 | $livewire->initialPreviewModalData( 30 | $this->evaluate($this->previewModalData) 31 | ); 32 | 33 | if ($this->shouldPreviewInNewTab()) { 34 | $livewire->openPreviewTab(); 35 | } else { 36 | $livewire->openPreviewModal(); 37 | } 38 | }); 39 | 40 | Support\View::setupPreviewModal(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Pages/Concerns/HasBuilderPreview.php: -------------------------------------------------------------------------------- 1 | listeners, [ 15 | 'updateBuilderFieldWithEditorData' => 'updateBuilderFieldWithEditorData', 16 | ]); 17 | } 18 | 19 | protected function getBuilderEditorTitle(): string 20 | { 21 | return __('filament-peek::ui.builder-editor-title'); 22 | } 23 | 24 | protected function getBuilderPreviewUrl(string $builderName): ?string 25 | { 26 | return null; 27 | } 28 | 29 | protected function getBuilderPreviewView(string $builderName): ?string 30 | { 31 | return null; 32 | } 33 | 34 | public static function getBuilderEditorSchema(string $builderName): Component|array 35 | { 36 | return []; 37 | } 38 | 39 | protected function mutateInitialBuilderEditorData(string $builderName, array $editorData): array 40 | { 41 | return $editorData; 42 | } 43 | 44 | public static function mutateBuilderPreviewData(string $builderName, array $editorData, array $previewData): array 45 | { 46 | return $previewData; 47 | } 48 | 49 | /** @internal */ 50 | public static function renderBuilderPreview(string $view, array $data): string 51 | { 52 | return Html::injectPreviewModalStyle( 53 | view($view, $data)->render() 54 | ); 55 | } 56 | 57 | /** @internal */ 58 | public function updateBuilderFieldWithEditorData(string $builderName, array $editorData): void 59 | { 60 | if (array_key_exists($builderName, $editorData)) { 61 | $this->data[$builderName] = $editorData[$builderName]; 62 | } 63 | 64 | if (class_exists('\FilamentTiptapEditor\TiptapEditor')) { 65 | $this->dispatch('refresh-tiptap-editors'); 66 | } 67 | } 68 | 69 | /** @internal */ 70 | protected function prepareBuilderEditorData(string $builderName): array 71 | { 72 | if (array_key_exists($builderName, $this->data)) { 73 | return $this->form->getStateOnly([$builderName]); 74 | } 75 | 76 | return []; 77 | } 78 | 79 | /** @internal */ 80 | public static function prepareBuilderPreviewData(array $data): array 81 | { 82 | $data['isPeekPreviewModal'] = true; 83 | 84 | return $data; 85 | } 86 | 87 | /** @internal */ 88 | public static function builderEditorHasSidebarActions(string $builderName): bool 89 | { 90 | $schema = static::getBuilderEditorSchema($builderName); 91 | 92 | return $schema instanceof Builder; 93 | } 94 | 95 | /** @internal */ 96 | public function openPreviewModalForBuidler(string $builderName): void 97 | { 98 | $this->checkCustomListener(); 99 | 100 | $editorData = $this->mutateInitialBuilderEditorData( 101 | $builderName, 102 | $this->prepareBuilderEditorData($builderName) 103 | ); 104 | 105 | $this->dispatch( 106 | 'openBuilderEditor', 107 | previewView: $this->getBuilderPreviewView($builderName), 108 | previewUrl: $this->getBuilderPreviewUrl($builderName), 109 | modalTitle: $this->getPreviewModalTitle(), 110 | editorTitle: $this->getBuilderEditorTitle(), 111 | editorData: $editorData, 112 | builderName: $builderName, 113 | pageClass: static::class, 114 | ); 115 | } 116 | 117 | private function checkCustomListener(): void 118 | { 119 | $hasCustomListener = collect($this->getListeners()) 120 | ->values() 121 | ->contains('updateBuilderFieldWithEditorData'); 122 | 123 | if (! $hasCustomListener) { 124 | throw new InvalidArgumentException("Missing 'updateBuilderFieldWithEditorData' Livewire event listener. Add it to your Page's `\$listeners` array."); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Pages/Concerns/HasPreviewModal.php: -------------------------------------------------------------------------------- 1 | shouldCallHooksBeforePreview; 52 | } 53 | 54 | protected function getShouldDehydrateBeforePreview(): bool 55 | { 56 | return $this->shouldDehydrateBeforePreview; 57 | } 58 | 59 | /** @internal */ 60 | public static function renderPreviewModalView(?string $view, array $data): string 61 | { 62 | return Support\Html::injectPreviewModalStyle( 63 | view($view, $data)->render() 64 | ); 65 | } 66 | 67 | /** @internal */ 68 | protected function preparePreviewModalData(): array 69 | { 70 | $shouldCallHooks = $this->getShouldCallHooksBeforePreview(); 71 | $shouldDehydrate = $this->getShouldDehydrateBeforePreview(); 72 | $record = null; 73 | 74 | if ($this->previewableRecord) { 75 | $record = $this->previewableRecord; 76 | } elseif (method_exists($this, 'mutateFormDataBeforeCreate')) { 77 | if (! $shouldCallHooks && $shouldDehydrate) { 78 | $this->form->validate(); 79 | $this->form->callBeforeStateDehydrated(); 80 | } 81 | $data = $this->mutateFormDataBeforeCreate($this->form->getState($shouldCallHooks)); 82 | $record = $this->getModel()::make($data); 83 | } elseif (method_exists($this, 'mutateFormDataBeforeSave')) { 84 | if (! $shouldCallHooks && $shouldDehydrate) { 85 | $this->form->validate(); 86 | $this->form->callBeforeStateDehydrated(); 87 | } 88 | $data = $this->mutateFormDataBeforeSave($this->form->getState($shouldCallHooks)); 89 | $record = $this->getRecord(); 90 | $record->fill($data); 91 | } elseif (method_exists($this, 'getRecord')) { 92 | $record = $this->getRecord(); 93 | } 94 | 95 | return array_merge( 96 | $this->initialPreviewModalData, 97 | [ 98 | $this->getPreviewModalDataRecordKey() => $record, 99 | 'isPeekPreviewModal' => true, 100 | ] 101 | ); 102 | } 103 | 104 | /** @internal */ 105 | public function openPreviewModal(): void 106 | { 107 | $previewModalUrl = null; 108 | $previewModalHtmlContent = null; 109 | 110 | try { 111 | $this->previewModalData = $this->mutatePreviewModalData($this->preparePreviewModalData()); 112 | 113 | if ($previewModalUrl = $this->getPreviewModalUrl()) { 114 | // pass 115 | } elseif ($view = $this->getPreviewModalView()) { 116 | if (config('filament-peek.internalPreviewUrl.enabled', false)) { 117 | $token = app(Support\Cache::class)->createPreviewToken(); 118 | 119 | CachedPreview::make(static::class, $view, $this->previewModalData) 120 | ->put($token, config('filament-peek.internalPreviewUrl.cacheDuration', 60)); 121 | 122 | $previewModalUrl = route('filament-peek.preview', ['token' => $token]); 123 | } else { 124 | $previewModalHtmlContent = static::renderPreviewModalView($view, $this->previewModalData); 125 | } 126 | } else { 127 | throw new InvalidArgumentException('Missing preview modal URL or Blade view.'); 128 | } 129 | } catch (Halt $exception) { 130 | $this->closePreviewModal(); 131 | 132 | return; 133 | } 134 | 135 | $this->dispatch( 136 | 'open-preview-modal', 137 | modalTitle: $this->getPreviewModalTitle(), 138 | iframeUrl: $previewModalUrl, 139 | iframeContent: $previewModalHtmlContent, 140 | ); 141 | } 142 | 143 | /** @internal */ 144 | public function openPreviewTab(): void 145 | { 146 | $previewModalUrl = null; 147 | 148 | if (! config('filament-peek.internalPreviewUrl.enabled')) { 149 | throw new PreviewModalException('You must enable the `internalPreviewUrl` configuration to open the preview in a new tab.'); 150 | } 151 | 152 | try { 153 | $this->previewModalData = $this->mutatePreviewModalData($this->preparePreviewModalData()); 154 | 155 | if ($previewModalUrl = $this->getPreviewModalUrl()) { 156 | // pass 157 | } elseif ($view = $this->getPreviewModalView()) { 158 | $token = app(Support\Cache::class)->createPreviewToken(); 159 | 160 | CachedPreview::make(static::class, $view, $this->previewModalData) 161 | ->put($token, config('filament-peek.internalPreviewUrl.cacheDuration', 60)); 162 | 163 | $previewModalUrl = route('filament-peek.preview', ['token' => $token]); 164 | } else { 165 | throw new InvalidArgumentException('Missing preview modal URL or Blade view.'); 166 | } 167 | } catch (Halt $exception) { 168 | return; 169 | } 170 | 171 | $this->dispatch('open-preview-tab', url: $previewModalUrl); 172 | } 173 | 174 | /** @internal */ 175 | public function closePreviewModal(): void 176 | { 177 | $this->dispatch('close-preview-modal'); 178 | } 179 | 180 | /** @internal */ 181 | public function setPreviewableRecord(Model $record): void 182 | { 183 | $this->previewableRecord = $record; 184 | } 185 | 186 | /** @internal */ 187 | public function initialPreviewModalData(array $data): void 188 | { 189 | $this->initialPreviewModalData = $data; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Support/Cache.php: -------------------------------------------------------------------------------- 1 | getAuthIdentifier().Config::get('app.key', '')); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Support/Concerns/CanPreviewInNewTab.php: -------------------------------------------------------------------------------- 1 | shouldPreviewInNewTab = $condition; 14 | 15 | return $this; 16 | } 17 | 18 | public function shouldPreviewInNewTab(): bool 19 | { 20 | return (bool) $this->evaluate($this->shouldPreviewInNewTab); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Support/Concerns/SetsInitialPreviewModalData.php: -------------------------------------------------------------------------------- 1 | previewModalData = $previewModalData; 14 | 15 | return $this; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Support/Html.php: -------------------------------------------------------------------------------- 1 | body { pointer-events: none !important; }'; 14 | 15 | return preg_replace('#\#', "{$style}", $htmlContent); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Support/Page.php: -------------------------------------------------------------------------------- 1 | hasPlugin(FilamentPeekPlugin::ID); 18 | } 19 | 20 | public static function ensurePluginIsLoaded(): void 21 | { 22 | if (! static::pluginIsLoaded()) { 23 | throw new PreviewModalException('The `FilamentPeekPlugin` class is not registered in the current Panel.'); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Support/View.php: -------------------------------------------------------------------------------- 1 | label(__('filament-peek::ui.preview-action-label')) 22 | ->icon('heroicon-s-eye') 23 | ->action(function ($livewire, $record) { 24 | Support\Panel::ensurePluginIsLoaded(); 25 | 26 | Support\Page::ensurePreviewModalSupport($livewire); 27 | 28 | $livewire->initialPreviewModalData( 29 | $this->evaluate($this->previewModalData) 30 | ); 31 | 32 | $livewire->setPreviewableRecord($record); 33 | 34 | $livewire->openPreviewModal(); 35 | }); 36 | 37 | Support\View::setupPreviewModal(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const preset = require('./vendor/filament/support/tailwind.config.preset') 2 | 3 | module.exports = { 4 | presets: [preset], 5 | content: [ 6 | './resources/views/**/*.blade.php', 7 | './src/**/*.php', 8 | ], 9 | } 10 | --------------------------------------------------------------------------------