├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── datastar.php ├── examples └── hello-world │ └── resources │ └── views │ ├── datastar │ └── hello-world.blade.php │ └── index.blade.php ├── phpunit.xml ├── public └── datastar │ └── 1.0.0-beta.11 │ ├── datastar.js │ └── datastar.js.map └── src ├── DatastarEventStream.php ├── DatastarServiceProvider.php ├── Helpers └── Datastar.php ├── Http ├── Controllers │ └── DatastarController.php └── Middleware │ └── RegisterScript.php ├── Models ├── Config.php └── Signals.php ├── Services └── Sse.php └── helpers.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes for Datastar 2 | 3 | ## 1.0.0-beta.8 - 2025-04-15 4 | 5 | ### Added 6 | 7 | - Added a `datastar()->getFragments()` helper method that fetches and merge fragments into the DOM. 8 | 9 | ## 1.0.0-beta.7 - 2025-04-09 10 | 11 | ### Changed 12 | 13 | - Update the Datastar library to version 1.0.0-beta.11. 14 | 15 | ## 1.0.0-beta.6 - 2025-03-01 16 | 17 | ### Changed 18 | 19 | - Update the Datastar library to version 1.0.0-beta.9. 20 | 21 | ## 1.0.0-beta.5 - 2025-02-25 22 | 23 | ### Changed 24 | 25 | - Update the Datastar library to version 1.0.0-beta.8. 26 | 27 | ## 1.0.0-beta.4 - 2025-02-14 28 | 29 | ### Changed 30 | 31 | - Update the Datastar library to version 1.0.0-beta.7. 32 | 33 | ## 1.0.0-beta.3 - 2025-02-12 34 | 35 | ### Changed 36 | 37 | - Extract methods into the SSE service class. 38 | 39 | ## 1.0.0-beta.2 - 2025-02-02 40 | 41 | ### Added 42 | 43 | - Added the `location` Blade directive. 44 | 45 | ## 1.0.0-beta.1 - 2025-01-13 46 | 47 | - Initial beta release. 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © PutYourLightsOn 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stable Version](https://img.shields.io/packagist/v/putyourlightson/laravel-datastar?label=stable)]((https://packagist.org/packages/putyourlightson/laravel-datastar)) 2 | [![Total Downloads](https://img.shields.io/packagist/dt/putyourlightson/laravel-datastar)](https://packagist.org/packages/putyourlightson/laravel-datastar) 3 | 4 |

5 | 6 | # Datastar Package for Laravel 7 | 8 | ### A reactive hypermedia framework for Laravel. 9 | 10 | > [!WARNING] 11 | > **This package is in beta and its API may change.** 12 | 13 | This package integrates the [Datastar hypermedia framework](https://data-star.dev/) with [Laravel](https://laravel.com/), allowing you to create reactive frontends driven by Blade views _or_ controllers. It aims to replace the need for front-end frameworks such as React, Vue.js and Alpine.js + htmx, and instead lets you manage state and use logic from your Laravel backend. 14 | 15 | Use-cases: 16 | 17 | - Live search and filtering 18 | - Loading more elements / Infinite scroll 19 | - Paginating, ordering and filtering lists 20 | - Submitting forms and running actions 21 | - Pretty much anything to do with reactive front-ends 22 | 23 | ## License 24 | 25 | This package is licensed for free under the MIT License. 26 | 27 | ## Requirements 28 | 29 | This package requires [Laravel](https://laravel.com/) 11.0.0 or later. 30 | 31 | ## Installation 32 | 33 | Install manually using composer, then run the `artisan vendor:publish --tag=public` command to publish the public assets. 34 | 35 | ```shell 36 | composer require putyourlightson/laravel-datastar:^1.0.0-beta.1 37 | 38 | php artisan vendor:publish --tag=public 39 | ``` 40 | 41 | ## Overview 42 | 43 | The Datastar package for Laravel allows you to handle backend requests by sending SSE events using [Blade directives](#blade-directives) in views _or_ [using controllers](#using-controllers). The former requires less setup and is more straightforward, while the latter provides more flexibility. 44 | 45 | Here’s a trivial example that toggles some backend state using the Blade view `datastar/toggle.blade.php` to handle the request. 46 | 47 | ```html 48 |
49 |
50 | 53 |
54 | ``` 55 | 56 | ```php 57 | {{-- datastar/toggle.blade.php --}} 58 | 59 | @php 60 | $enabled = $signals->enabled; 61 | // Do something with the state and toggle the enabled state. 62 | $enabled = !$enabled; 63 | @endphp 64 | 65 | @mergesignals(['enabled' => $enabled]) 66 | 67 | @mergefragments 68 | 69 | {{ $enabled ? 'Disable' : 'Enable' }} 70 | 71 | @endmergefragments 72 | ``` 73 | 74 | ## Usage 75 | 76 | Start by reading the [Getting Started](https://data-star.dev/guide/getting_started) guide to learn how to use Datastar on the frontend. The Datastar package for Laravel only handles backend requests. 77 | 78 | > [!NOTE] 79 | > The Datastar [VSCode extension](https://marketplace.visualstudio.com/items?itemName=starfederation.datastar-vscode) and [IntelliJ plugin](https://plugins.jetbrains.com/plugin/26072-datastar-support) have autocomplete for all `data-*` attributes. 80 | 81 | When working with signals, note that you can convert a PHP array into a JSON object using the `json_encode` function. 82 | 83 | ```php 84 | @php 85 | $signals = ['foo' => 1, 'bar' => 2]; 86 | @endphp 87 | 88 |
89 | ``` 90 | 91 | ### Datastar Helper 92 | 93 | The `datastar()` helper function is available in Blade views and returns a `Datastar` helper that can be used to generate action requests to the Datastar controller. The Datastar controller renders a view containing one or [Blade directives](#blade-directives) that each send an SSE event. [Signals](#signals) are also sent as part of the request, and are made available in Datastar views using the `$signals` variable. 94 | 95 | #### `datastar()->get()` 96 | 97 | Returns a `@get()` action request to render a view at the given path. The value can be a file path _or_ a dot-separated path to a Blade view. 98 | 99 | ```php 100 | {{ datastar()->get('path.to.view') }} 101 | ``` 102 | 103 | Variables can be passed into the view using a second argument. Any variables passed in will become available in the rendered view. Variables are tamper-proof yet visible in the source code in plain text, so you should avoid passing in any sensitive data. 104 | 105 | ```php 106 | {{ datastar()->get('path.to.view', ['offset' => 10]) }} 107 | ``` 108 | 109 | #### `datastar()->post()` 110 | 111 | Works the same as [`datastar()->get()`](#datastar-get) but returns a `@post()` action request to render a view at the given path. A CSRF token is automatically generated and sent along with the request. 112 | 113 | ```php 114 | {{ datastar()->post('path.to.view') }} 115 | ``` 116 | 117 | #### `datastar()->put()` 118 | 119 | Works the same as [`datastar()->post()`](#datastar-post) but returns a `@put()` action request. 120 | 121 | ```php 122 | {{ datastar()->put('path.to.view') }} 123 | ``` 124 | 125 | #### `datastar()->patch()` 126 | 127 | Works the same as [`datastar()->post()`](#datastar-post) but returns a `@patch()` action request. 128 | 129 | ```php 130 | {{ datastar()->patch('path.to.view') }} 131 | ``` 132 | 133 | #### `datastar()->delete()` 134 | 135 | Works the same as [`datastar()->post()`](#datastar-post) but returns a `@delete()` action request. 136 | 137 | ```php 138 | {{ datastar()->delete('path.to.view') }} 139 | ``` 140 | 141 | #### `datastar()->getFragments()` 142 | 143 | Returns a `@get()` action request to render and automatically _merge_ the fragments contained in a view at the given path. The view should contain one or more fragments to be merged. 144 | 145 | ```php 146 | {{ datastar()->getFragments('path.to.view') }} 147 | ``` 148 | 149 | ```html 150 |
The view should contain one or more fragments to be merged.
151 | ``` 152 | 153 | ### Blade Directives 154 | 155 | #### `@mergefragments` 156 | 157 | Merges one or more fragments into the DOM. 158 | 159 | ```php 160 | @mergefragments 161 |
New fragment
162 | @endmergefragments 163 | ``` 164 | 165 | #### `@removefragments` 166 | 167 | Removes one or more HTML fragments that match the provided selector from the DOM. 168 | 169 | ```php 170 | @removefragments('#old-fragment') 171 | ``` 172 | 173 | #### `@mergesignals` 174 | 175 | Updates the signals with new values. 176 | 177 | ```php 178 | @mergesignals(['foo' => 1, 'bar' => 2]) 179 | ``` 180 | 181 | #### `@removesignals` 182 | 183 | Removes signals that match one or more provided paths. 184 | 185 | ```php 186 | @removesignals(['foo', 'bar']) 187 | ``` 188 | 189 | #### `@executescript` 190 | 191 | Executes JavaScript in the browser. 192 | 193 | ```php 194 | @executescript 195 | alert('Hello, world!'); 196 | @endexecutescript 197 | ``` 198 | 199 | #### `@location` 200 | 201 | Redirects the browser by setting the location to the provided URI. 202 | 203 | ```php 204 | @location('/guide') 205 | ``` 206 | 207 | ### Using Controllers 208 | 209 | You can send SSE events using your own controller _instead_ of the Datastar controller by using the `DatastarEventStream` trait. Return the `getStreamedResponse()` method, passing a callable into it that sends zero or more SSE events using methods provided. 210 | 211 | ```php 212 | // routes/web.php 213 | 214 | use App\Http\Controllers\MyController; 215 | 216 | Route::resource('/my-controller', MyController::class); 217 | ``` 218 | 219 | ```php 220 | namespace App\Http\Controllers; 221 | 222 | use Illuminate\Routing\Controller; 223 | use Putyourlightson\Datastar\DatastarEventStream; 224 | use Symfony\Component\HttpFoundation\StreamedResponse; 225 | 226 | class MyController extends Controller 227 | { 228 | use DatastarEventStream; 229 | 230 | public function index(): StreamedResponse 231 | { 232 | return $this->getStreamedResponse(function() { 233 | $signals = $this->getSignals(); 234 | $this->mergeSignals(['enabled' => $signals->enabled ? false : true]); 235 | $this->mergeFragments(' 236 | ' . ($signals->enabled ? 'Enable' : 'Disable') . ' 237 | '); 238 | }); 239 | } 240 | } 241 | ``` 242 | 243 | ### DatastarEventStream Trait 244 | 245 | #### `mergeFragments()` 246 | 247 | Merges one or more fragments into the DOM. 248 | 249 | ```php 250 | $this->mergeFragments('
New fragment
'); 251 | ``` 252 | 253 | #### `removeFragments()` 254 | 255 | Removes one or more HTML fragments that match the provided selector from the DOM. 256 | 257 | ```php 258 | $this->removeFragments('#old-fragment'); 259 | ``` 260 | 261 | #### `mergeSignals()` 262 | 263 | Updates the signals with new values. 264 | 265 | ```php 266 | $this->mergeSignals(['foo' => 1, 'bar' => 2]); 267 | ``` 268 | 269 | #### `removeSignals()` 270 | 271 | Removes signals that match one or more provided paths. 272 | 273 | ```php 274 | $this->removeSignals(['foo', 'bar']); 275 | ``` 276 | 277 | #### `executeScript()` 278 | 279 | Executes JavaScript in the browser. 280 | 281 | ```php 282 | $this->executeScript('alert("Hello, world!")'); 283 | ``` 284 | 285 | #### `location()` 286 | 287 | Redirects the browser by setting the location to the provided URI. 288 | 289 | ```php 290 | $this->location('/guide'); 291 | ``` 292 | 293 | #### `renderDatastarView()` 294 | 295 | Renders a Datastar view. 296 | 297 | ```php 298 | $this->renderDatastarView('datastar.toggle', ['enabled' => true]); 299 | ``` 300 | 301 | ### Signals 302 | 303 | When working with signals, either in views rendered by the Datastar controller or by calling `$this->getSignals()`, you are working with a [Signals model](https://github.com/putyourlightson/laravel-datastar/blob/develop/src/Models/Signals.php), which provides a simple way to manage signals. 304 | 305 | ```php 306 | @php 307 | // Getting signal values. 308 | $username = $signals->username; 309 | $username = $signals->get('username'); 310 | $username = $signals->get('user.username'); 311 | 312 | // Setting signal values. 313 | $username = $signals->username('bobby'); 314 | $username = $signals->set('username', 'bobby'); 315 | $username = $signals->set('user.username', 'bobby'); 316 | $username = $signals->setValues(['user.username' => 'bobby', 'success' => true]); 317 | 318 | // Removing signal values. 319 | $username = $signals->remove('username'); 320 | $username = $signals->remove('user.username'); 321 | @endphp 322 | ``` 323 | 324 | > [!NOTE] 325 | > Signals updates _cannot_ be wrapped in `{% mergefragment %}` tags, since each update creates a server-sent event which will conflict with the fragment’s contents. 326 | 327 | --- 328 | 329 | Created by [PutYourLightsOn](https://putyourlightson.com/). 330 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "putyourlightson/laravel-datastar", 3 | "description": "A reactive hypermedia framework for Laravel.", 4 | "version": "1.0.0-beta.8", 5 | "type": "library", 6 | "license": "mit", 7 | "require": { 8 | "php": "^8.2", 9 | "laravel/framework": "^11.0", 10 | "starfederation/datastar-php": "1.0.0-beta.17" 11 | }, 12 | "require-dev": { 13 | "craftcms/ecs": "dev-main", 14 | "craftcms/phpstan": "dev-main", 15 | "pestphp/pest": "^3.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Putyourlightson\\Datastar\\": "src/" 20 | }, 21 | "files": [ 22 | "src/helpers.php" 23 | ] 24 | }, 25 | "extra": { 26 | "laravel": { 27 | "providers": [ 28 | "Putyourlightson\\Datastar\\DatastarServiceProvider" 29 | ] 30 | } 31 | }, 32 | "scripts": { 33 | "check-cs": "ecs check --ansi", 34 | "fix-cs": "ecs check --ansi --fix", 35 | "phpstan": "phpstan --memory-limit=1G", 36 | "test": "vendor/bin/pest" 37 | }, 38 | "config": { 39 | "allow-plugins": { 40 | "pestphp/pest-plugin": true 41 | }, 42 | "optimize-autoloader": true, 43 | "sort-packages": true 44 | }, 45 | "support": { 46 | "docs": "https://github.com/putyourlightson/laravel-datastar", 47 | "source": "https://github.com/putyourlightson/laravel-datastar", 48 | "issues": "https://github.com/putyourlightson/laravel-datastar/issues" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config/datastar.php: -------------------------------------------------------------------------------- 1 | true, 11 | 12 | /** 13 | * The name of the signals variable that will be injected into Datastar templates. 14 | */ 15 | 'signalsVariableName' => 'signals', 16 | 17 | /** 18 | * The event options to override the Datastar defaults. Null values will be ignored. 19 | */ 20 | 'defaultEventOptions' => [ 21 | 'retryDuration' => 1000, 22 | ], 23 | 24 | /** 25 | * The fragment options to override the Datastar defaults. Null values will be ignored. 26 | */ 27 | 'defaultFragmentOptions' => [ 28 | 'settleDuration' => null, 29 | 'useViewTransition' => null, 30 | ], 31 | 32 | /** 33 | * The signal options to override the Datastar defaults. Null values will be ignored. 34 | */ 35 | 'defaultSignalOptions' => [ 36 | 'onlyIfMissing' => null, 37 | ], 38 | 39 | /** 40 | * The execute script options to override the Datastar defaults. Null values will be ignored. 41 | */ 42 | 'defaultExecuteScriptOptions' => [ 43 | 'autoRemove' => null, 44 | 'attributes' => null, 45 | ], 46 | ]; 47 | -------------------------------------------------------------------------------- /examples/hello-world/resources/views/datastar/hello-world.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | /** @var \Putyourlightson\Datastar\Models\Signals $signals */ 3 | $delay = $signals->get('delay', 0); 4 | $message = 'Hello, world!'; 5 | @endphp 6 | 7 | @for ($i = 0; $i < strlen($message); $i++) 8 | @mergefragments 9 |
10 | {{ substr($message, 0, $i + 1) }} 11 |
12 | @endmergefragments 13 | @php 14 | // Sleep for the provided delay in milliseconds. 15 | usleep($delay * 1000); 16 | @endphp 17 | @endfor 18 | -------------------------------------------------------------------------------- /examples/hello-world/resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Datastar SDK Demo 5 | 6 | 7 | 8 |
9 |
10 |

11 | Datastar SDK Demo 12 |

13 | Rocket 14 |
15 |

16 | SSE events will be streamed from the backend to the frontend. 17 |

18 |
19 | 22 | 23 |
24 | 27 |
28 |
29 |
Hello, world!
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | tests 10 | 11 | 12 | 13 | 14 | app 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /public/datastar/1.0.0-beta.11/datastar.js: -------------------------------------------------------------------------------- 1 | // Datastar v1.0.0-beta.11 2 | var et=/🖕JS_DS🚀/.source,_e=et.slice(0,5),le=et.slice(4),q="datastar",tt="Datastar-Request",nt=1e3,rt="type module",Re=!1,it=!1,ot=!0,U={Morph:"morph",Inner:"inner",Outer:"outer",Prepend:"prepend",Append:"append",Before:"before",After:"after",UpsertAttributes:"upsertAttributes"},st=U.Morph,F={MergeFragments:"datastar-merge-fragments",MergeSignals:"datastar-merge-signals",RemoveFragments:"datastar-remove-fragments",RemoveSignals:"datastar-remove-signals",ExecuteScript:"datastar-execute-script"};var x=(r=>(r[r.Attribute=1]="Attribute",r[r.Watcher=2]="Watcher",r[r.Action=3]="Action",r))(x||{});var ye=`${q}-signals`;var X=t=>t.trim()==="true",Y=t=>t.replace(/[A-Z]+(?![a-z])|[A-Z]/g,(e,n)=>(n?"-":"")+e.toLowerCase()),ve=t=>Y(t).replace(/-./g,e=>e[1].toUpperCase()),qe=t=>Y(t).replace(/-/g,"_"),Sn=t=>ve(t).replace(/^./,e=>e[0].toUpperCase()),xe=t=>new Function(`return Object.assign({}, ${t})`)(),Z=t=>t.startsWith("$")?t.slice(1):t,En={kebab:Y,snake:qe,pascal:Sn};function H(t,e){for(let n of e.get("case")||[]){let r=En[n];r&&(t=r(t))}return t}var Tn="computed",at={type:1,name:Tn,keyReq:1,valReq:1,onLoad:({key:t,mods:e,signals:n,genRX:r})=>{t=H(t,e);let i=r();n.setComputed(t,i)}};var lt={type:1,name:"signals",onLoad:t=>{let{key:e,mods:n,signals:r,value:i,genRX:o}=t,s=n.has("ifmissing");if(e!==""){let a=H(e,n),p=i===""?i:o()();s?r.upsertIfMissing(a,p):r.setValue(a,p)}else{let a=xe(t.value);t.value=JSON.stringify(a);let y=o()();r.merge(y,s)}}};var ut={type:1,name:"star",keyReq:2,valReq:2,onLoad:()=>{alert("YOU ARE PROBABLY OVERCOMPLICATING IT")}};var ue=class{#e=0;#t;constructor(e=q){this.#t=e}with(e){if(typeof e=="string")for(let n of e.split(""))this.with(n.charCodeAt(0));else typeof e=="boolean"?this.with(1<<(e?7:3)):this.#e=this.#e*33^e;return this}get value(){return this.#e}get string(){return this.#t+Math.abs(this.#e).toString(36)}};function we(t){if(t.id)return t.id;let e=new ue,n=t;for(;n;){if(e.with(n.tagName||""),n.id){e.with(n.id);break}let r=n?.parentNode;r&&e.with([...r.children].indexOf(n)),n=r}return e.string}function Me(t,e){return new ue().with(t).with(e).value}function be(t,e){if(!t||!(t instanceof HTMLElement||t instanceof SVGElement))return null;let n=t.dataset;if("starIgnore"in n)return null;"starIgnore__self"in n||e(t);let r=t.firstElementChild;for(;r;)be(r,e),r=r.nextElementSibling}var An="https://data-star.dev/errors";function $e(t,e,n={}){let r=new Error;r.name=`${q} ${t} error`;let i=qe(e),o=new URLSearchParams({metadata:JSON.stringify(n)}).toString(),s=JSON.stringify(n,null,2);return r.message=`${e} 3 | More info: ${An}/${t}/${i}?${o} 4 | Context: ${s}`,r}function j(t,e,n={}){return $e("internal",e,Object.assign({from:t},n))}function $(t,e,n={}){let r={plugin:{name:e.plugin.name,type:x[e.plugin.type]}};return $e("init",t,Object.assign(r,n))}function P(t,e,n={}){let r={plugin:{name:e.plugin.name,type:x[e.plugin.type]},element:{id:e.el.id,tag:e.el.tagName},expression:{rawKey:e.rawKey,key:e.key,value:e.value,validSignals:e.signals.paths(),fnContent:e.fnContent}};return $e("runtime",t,Object.assign(r,n))}var ce="preact-signals",_n=Symbol.for("preact-signals"),K=1,fe=2,Ee=4,pe=8,Pe=16,de=32;function Be(){Ce++}function Ge(){if(Ce>1){Ce--;return}let t,e=!1;for(;Se!==void 0;){let n=Se;for(Se=void 0,We++;n!==void 0;){let r=n._nextBatchedEffect;if(n._nextBatchedEffect=void 0,n._flags&=~fe,!(n._flags&pe)&&ft(n))try{n._callback()}catch(i){e||(t=i,e=!0)}n=r}}if(We=0,Ce--,e)throw t}var C;var Se,Ce=0,We=0,Ne=0;function ct(t){if(C===void 0)return;let e=t._node;if(e===void 0||e._target!==C)return e={_version:0,_source:t,_prevSource:C._sources,_nextSource:void 0,_target:C,_prevTarget:void 0,_nextTarget:void 0,_rollbackNode:e},C._sources!==void 0&&(C._sources._nextSource=e),C._sources=e,t._node=e,C._flags&de&&t._subscribe(e),e;if(e._version===-1)return e._version=0,e._nextSource!==void 0&&(e._nextSource._prevSource=e._prevSource,e._prevSource!==void 0&&(e._prevSource._nextSource=e._nextSource),e._prevSource=C._sources,e._nextSource=void 0,C._sources._nextSource=e,C._sources=e),e}function I(t){this._value=t,this._version=0,this._node=void 0,this._targets=void 0}I.prototype.brand=_n;I.prototype._refresh=()=>!0;I.prototype._subscribe=function(t){this._targets!==t&&t._prevTarget===void 0&&(t._nextTarget=this._targets,this._targets!==void 0&&(this._targets._prevTarget=t),this._targets=t)};I.prototype._unsubscribe=function(t){if(this._targets!==void 0){let e=t._prevTarget,n=t._nextTarget;e!==void 0&&(e._nextTarget=n,t._prevTarget=void 0),n!==void 0&&(n._prevTarget=e,t._nextTarget=void 0),t===this._targets&&(this._targets=n)}};I.prototype.subscribe=function(t){return me(()=>{let e=this.value,n=C;C=void 0;try{t(e)}finally{C=n}})};I.prototype.valueOf=function(){return this.value};I.prototype.toString=function(){return`${this.value}`};I.prototype.toJSON=function(){return this.value};I.prototype.peek=function(){let t=C;C=void 0;try{return this.value}finally{C=t}};Object.defineProperty(I.prototype,"value",{get(){let t=ct(this);return t!==void 0&&(t._version=this._version),this._value},set(t){if(t!==this._value){if(We>100)throw j(ce,"SignalCycleDetected");let e=this._value,n=t;this._value=t,this._version++,Ne++,Be();try{for(let r=this._targets;r!==void 0;r=r._nextTarget)r._target._notify()}finally{Ge()}this?._onChange({old:e,revised:n})}}});function ft(t){for(let e=t._sources;e!==void 0;e=e._nextSource)if(e._source._version!==e._version||!e._source._refresh()||e._source._version!==e._version)return!0;return!1}function dt(t){for(let e=t._sources;e!==void 0;e=e._nextSource){let n=e._source._node;if(n!==void 0&&(e._rollbackNode=n),e._source._node=e,e._version=-1,e._nextSource===void 0){t._sources=e;break}}}function pt(t){let e=t._sources,n;for(;e!==void 0;){let r=e._prevSource;e._version===-1?(e._source._unsubscribe(e),r!==void 0&&(r._nextSource=e._nextSource),e._nextSource!==void 0&&(e._nextSource._prevSource=r)):n=e,e._source._node=e._rollbackNode,e._rollbackNode!==void 0&&(e._rollbackNode=void 0),e=r}t._sources=n}function ne(t){I.call(this,void 0),this._fn=t,this._sources=void 0,this._globalVersion=Ne-1,this._flags=Ee}ne.prototype=new I;ne.prototype._refresh=function(){if(this._flags&=~fe,this._flags&K)return!1;if((this._flags&(Ee|de))===de||(this._flags&=~Ee,this._globalVersion===Ne))return!0;if(this._globalVersion=Ne,this._flags|=K,this._version>0&&!ft(this))return this._flags&=~K,!0;let t=C;try{dt(this),C=this;let e=this._fn();(this._flags&Pe||this._value!==e||this._version===0)&&(this._value=e,this._flags&=~Pe,this._version++)}catch(e){this._value=e,this._flags|=Pe,this._version++}return C=t,pt(this),this._flags&=~K,!0};ne.prototype._subscribe=function(t){if(this._targets===void 0){this._flags|=Ee|de;for(let e=this._sources;e!==void 0;e=e._nextSource)e._source._subscribe(e)}I.prototype._subscribe.call(this,t)};ne.prototype._unsubscribe=function(t){if(this._targets!==void 0&&(I.prototype._unsubscribe.call(this,t),this._targets===void 0)){this._flags&=~de;for(let e=this._sources;e!==void 0;e=e._nextSource)e._source._unsubscribe(e)}};ne.prototype._notify=function(){if(!(this._flags&fe)){this._flags|=Ee|fe;for(let t=this._targets;t!==void 0;t=t._nextTarget)t._target._notify()}};Object.defineProperty(ne.prototype,"value",{get(){if(this._flags&K)throw j(ce,"SignalCycleDetected");let t=ct(this);if(this._refresh(),t!==void 0&&(t._version=this._version),this._flags&Pe)throw j(ce,"GetComputedError",{value:this._value});return this._value}});function mt(t){return new ne(t)}function gt(t){let e=t._cleanup;if(t._cleanup=void 0,typeof e=="function"){Be();let n=C;C=void 0;try{e()}catch(r){throw t._flags&=~K,t._flags|=pe,Ue(t),j(ce,"CleanupEffectError",{error:r})}finally{C=n,Ge()}}}function Ue(t){for(let e=t._sources;e!==void 0;e=e._nextSource)e._source._unsubscribe(e);t._fn=void 0,t._sources=void 0,gt(t)}function Rn(t){if(C!==this)throw j(ce,"EndEffectError");pt(this),C=t,this._flags&=~K,this._flags&pe&&Ue(this),Ge()}function Te(t){this._fn=t,this._cleanup=void 0,this._sources=void 0,this._nextBatchedEffect=void 0,this._flags=de}Te.prototype._callback=function(){let t=this._start();try{if(this._flags&pe||this._fn===void 0)return;let e=this._fn();typeof e=="function"&&(this._cleanup=e)}finally{t()}};Te.prototype._start=function(){if(this._flags&K)throw j(ce,"SignalCycleDetected");this._flags|=K,this._flags&=~pe,gt(this),dt(this),Be();let t=C;return C=this,Rn.bind(this,t)};Te.prototype._notify=function(){this._flags&fe||(this._flags|=fe,this._nextBatchedEffect=Se,Se=this)};Te.prototype._dispose=function(){this._flags|=pe,this._flags&K||Ue(this)};function me(t){let e=new Te(t);try{e._callback()}catch(n){throw e._dispose(),n}return e._dispose.bind(e)}var ht="namespacedSignals",ge=t=>{document.dispatchEvent(new CustomEvent(ye,{detail:Object.assign({added:[],removed:[],updated:[]},t)}))};function yt(t,e=!1){let n={};for(let r in t)if(Object.hasOwn(t,r)){if(e&&r.startsWith("_"))continue;let i=t[r];i instanceof I?n[r]=i.value:n[r]=yt(i)}return n}function vt(t,e,n=!1){let r={added:[],removed:[],updated:[]};for(let i in e)if(Object.hasOwn(e,i)){if(i.match(/\_\_+/))throw j(ht,"InvalidSignalKey",{key:i});let o=e[i];if(o instanceof Object&&!Array.isArray(o)){t[i]||(t[i]={});let s=vt(t[i],o,n);r.added.push(...s.added.map(a=>`${i}.${a}`)),r.removed.push(...s.removed.map(a=>`${i}.${a}`)),r.updated.push(...s.updated.map(a=>`${i}.${a}`))}else{if(Object.hasOwn(t,i)){if(n)continue;let p=t[i];if(p instanceof I){let y=p.value;p.value=o,y!==o&&r.updated.push(i);continue}}let a=new I(o);a._onChange=()=>{ge({updated:[i]})},t[i]=a,r.added.push(i)}}return r}function bt(t,e){for(let n in t)if(Object.hasOwn(t,n)){let r=t[n];r instanceof I?e(n,r):bt(r,(i,o)=>{e(`${n}.${i}`,o)})}}function xn(t,...e){let n={};for(let r of e){let i=r.split("."),o=t,s=n;for(let p=0;pn());this.setSignal(e,r)}value(e){return this.signal(e)?.value}setValue(e,n){let{signal:r}=this.upsertIfMissing(e,n),i=r.value;r.value=n,i!==n&&ge({updated:[e]})}upsertIfMissing(e,n){let r=e.split("."),i=this.#e;for(let p=0;p{ge({updated:[e]})},i[o]=a,ge({added:[e]}),{signal:a,inserted:!0}}remove(...e){if(!e.length){this.#e={};return}let n=Array();for(let r of e){let i=r.split("."),o=this.#e;for(let a=0;ae.push(n)),e}values(e=!1){return yt(this.#e,e)}JSON(e=!0,n=!1){let r=this.values(n);return e?JSON.stringify(r,null,2):JSON.stringify(r)}toString(){return this.JSON()}};var St=new ke,Ie={},Ke=[],re=new Map,je=null,Je="";function Et(t){Je=t}function Ve(...t){for(let e of t){let n={plugin:e,signals:St,effect:i=>me(i),actions:Ie,removals:re,applyToElement:Le},r;switch(e.type){case 3:{Ie[e.name]=e;break}case 1:{let i=e;Ke.push(i),r=i.onGlobalInit;break}case 2:{r=e.onGlobalInit;break}default:throw $("InvalidPluginType",n)}r&&r(n)}Ke.sort((e,n)=>{let r=n.name.length-e.name.length;return r!==0?r:e.name.localeCompare(n.name)})}function ze(){queueMicrotask(()=>{Le(document.documentElement),wn()})}function Le(t){be(t,e=>{let n=new Array,r=re.get(e.id)||new Map,i=new Map([...r]),o=new Map;for(let s of Object.keys(e.dataset)){if(!s.startsWith(Je))break;let a=e.dataset[s]||"",p=Me(s,a);o.set(s,p),r.has(p)?i.delete(p):n.push(s)}for(let[s,a]of i)a();for(let s of n){let a=o.get(s);Mn(e,s,a)}})}function wn(){je||(je=new MutationObserver(t=>{let e=new Set,n=new Set;for(let{target:r,type:i,addedNodes:o,removedNodes:s}of t)switch(i){case"childList":{for(let a of s)e.add(a);for(let a of o)n.add(a)}break;case"attributes":{n.add(r);break}}for(let r of e){let i=re.get(r.id);if(i){for(let[o,s]of i)s(),i.delete(o);i.size===0&&re.delete(r.id)}}for(let r of n)Le(r)}),je.observe(document.body,{attributes:!0,attributeOldValue:!0,childList:!0,subtree:!0}))}function Mn(t,e,n){let r=ve(e.slice(Je.length)),i=Ke.find(T=>new RegExp(`^${T.name}([A-Z]|_|$)`).test(r));if(!i)return;t.id.length||(t.id=we(t));let[o,...s]=r.slice(i.name.length).split(/\_\_+/),a=o.length>0;a&&(o=ve(o));let p=t.dataset[e]||"",y=p.length>0,v={signals:St,applyToElement:Le,effect:T=>me(T),actions:Ie,removals:re,genRX:()=>Pn(v,...i.argNames||[]),plugin:i,el:t,rawKey:r,key:o,value:p,mods:new Map},k=i.keyReq||0;if(a){if(k===2)throw P(`${i.name}KeyNotAllowed`,v)}else if(k===1)throw P(`${i.name}KeyRequired`,v);let b=i.valReq||0;if(y){if(b===2)throw P(`${i.name}ValueNotAllowed`,v)}else if(b===1)throw P(`${i.name}ValueRequired`,v);if(k===3||b===3){if(a&&y)throw P(`${i.name}KeyAndValueProvided`,v);if(!a&&!y)throw P(`${i.name}KeyOrValueRequired`,v)}for(let T of s){let[_,...E]=T.split(".");v.mods.set(ve(_),new Set(E.map(c=>c.toLowerCase())))}let A=i.onLoad(v)??(()=>{}),h=re.get(t.id);h||(h=new Map,re.set(t.id,h)),h.set(n,A)}function Pn(t,...e){let n="",r=/(\/(\\\/|[^\/])*\/|"(\\"|[^\"])*"|'(\\'|[^'])*'|`(\\`|[^`])*`|[^;])+/gm,i=t.value.trim().match(r);if(i){let A=i.length-1,h=i[A].trim();h.startsWith("return")||(i[A]=`return (${h});`),n=i.join(`; 5 | `)}let o=new Map,s=new RegExp(`(?:${_e})(.*?)(?:${le})`,"gm");for(let A of n.matchAll(s)){let h=A[1],T=new ue("dsEscaped").with(h).string;o.set(T,h),n=n.replace(_e+h+le,T)}let a=/@(\w*)\(/gm,p=n.matchAll(a),y=new Set;for(let A of p)y.add(A[1]);let v=new RegExp(`@(${Object.keys(Ie).join("|")})\\(`,"gm");n=n.replaceAll(v,"ctx.actions.$1.fn(ctx,");let k=t.signals.paths();if(k.length){let A=new RegExp(`\\$(${k.join("|")})(\\W|$)`,"gm");n=n.replaceAll(A,"ctx.signals.signal('$1').value$2")}for(let[A,h]of o)n=n.replace(A,h);let b=`return (() => { 6 | ${n} 7 | })()`;t.fnContent=b;try{let A=new Function("ctx",...e,b);return(...h)=>{try{return A(t,...h)}catch(T){throw P("ExecuteExpression",t,{error:T.message})}}}catch(A){throw P("GenerateExpression",t,{error:A.message})}}Ve(ut,lt,at);var ie=`${q}-sse`,De="started",Oe="finished",Tt="error",At="retrying",_t="retries-failed";function J(t,e){document.addEventListener(ie,n=>{if(n.detail.type!==t)return;let{argsRaw:r}=n.detail;e(r)})}function Q(t,e,n){document.dispatchEvent(new CustomEvent(ie,{detail:{type:t,elId:e,argsRaw:n}}))}async function Cn(t,e){let n=t.getReader(),r;for(;!(r=await n.read()).done;)e(r.value)}function Nn(t){let e,n,r,i=!1;return function(s){e===void 0?(e=s,n=0,r=-1):e=In(e,s);let a=e.length,p=0;for(;n0){let p=i.decode(s.subarray(0,a)),y=a+(s[a+1]===32?2:1),v=i.decode(s.subarray(y));switch(p){case"data":r.data=r.data?`${r.data} 8 | ${v}`:v;break;case"event":r.event=v;break;case"id":t(r.id=v);break;case"retry":{let k=Number.parseInt(v,10);Number.isNaN(k)||e(r.retry=k);break}}}}}function In(t,e){let n=new Uint8Array(t.length+e.length);return n.set(t),n.set(e,t.length),n}function Rt(){return{data:"",event:"",id:"",retry:void 0}}var Vn="text/event-stream",xt="last-event-id";function wt(t,e,{signal:n,headers:r,onopen:i,onmessage:o,onclose:s,onerror:a,openWhenHidden:p,fetch:y,retryInterval:v=1e3,retryScaler:k=2,retryMaxWaitMs:b=3e4,retryMaxCount:A=10,...h}){return new Promise((T,_)=>{let E=0,c={...r};c.accept||(c.accept=Vn);let d;function u(){d.abort(),document.hidden||S()}p||document.addEventListener("visibilitychange",u);let l=0;function g(){document.removeEventListener("visibilitychange",u),window.clearTimeout(l),d.abort()}n?.addEventListener("abort",()=>{g(),T()});let m=y??window.fetch,f=i??function(){};async function S(){d=new AbortController;try{let w=await m(t,{...h,headers:c,signal:d.signal});await f(w),await Cn(w.body,Nn(kn(R=>{R?c[xt]=R:delete c[xt]},R=>{v=R},o))),s?.(),g(),T()}catch(w){if(!d.signal.aborted)try{let R=a?.(w)??v;window.clearTimeout(l),l=window.setTimeout(S,R),v*=k,v=Math.min(v,b),E++,E>A?(Q(_t,e,{}),g(),_("Max retries reached.")):console.error(`Datastar failed to reach ${t.toString()} retrying in ${R}ms.`)}catch(R){g(),_(R)}}}S()})}var Mt=t=>`${t}`.includes("text/event-stream"),z=async(t,e,n,r)=>{let{el:i,signals:o}=t,s=i.id,{headers:a,contentType:p,includeLocal:y,selector:v,openWhenHidden:k,retryInterval:b,retryScaler:A,retryMaxWaitMs:h,retryMaxCount:T,abort:_}=Object.assign({headers:{},contentType:"json",includeLocal:!1,selector:null,openWhenHidden:!1,retryInterval:nt,retryScaler:2,retryMaxWaitMs:3e4,retryMaxCount:10,abort:void 0},r),E=e.toLowerCase(),c=()=>{};try{if(Q(De,s,{}),!n?.length)throw P("SseNoUrlProvided",t,{action:E});let d={};d[tt]=!0,p==="json"&&(d["Content-Type"]="application/json");let u=Object.assign({},d,a),l={method:e,headers:u,openWhenHidden:k,retryInterval:b,retryScaler:A,retryMaxWaitMs:h,retryMaxCount:T,signal:_,onopen:async f=>{if(f.status>=400){let S=f.status.toString();Q(Tt,s,{status:S})}},onmessage:f=>{if(!f.event.startsWith(q))return;let S=f.event,w={},R=f.data.split(` 9 | `);for(let L of R){let N=L.indexOf(" "),O=L.slice(0,N),D=w[O];D||(D=[],w[O]=D);let B=L.slice(N+1);D.push(B)}let M={};for(let[L,N]of Object.entries(w))M[L]=N.join(` 10 | `);Q(S,s,M)},onerror:f=>{if(Mt(f))throw P("InvalidContentType",t,{url:n});f&&(console.error(f.message),Q(At,s,{message:f.message}))}},g=new URL(n,window.location.origin),m=new URLSearchParams(g.search);if(p==="json"){let f=o.JSON(!1,!y);e==="GET"?m.set(q,f):l.body=f}else if(p==="form"){let f=v?document.querySelector(v):i.closest("form");if(f===null)throw v?P("SseFormNotFound",t,{action:E,selector:v}):P("SseClosestFormNotFound",t,{action:E});if(i!==f){let w=R=>R.preventDefault();f.addEventListener("submit",w),c=()=>f.removeEventListener("submit",w)}if(!f.checkValidity()){f.reportValidity(),c();return}let S=new FormData(f);if(e==="GET"){let w=new URLSearchParams(S);for(let[R,M]of w)m.set(R,M)}else l.body=S}else throw P("SseInvalidContentType",t,{action:E,contentType:p});g.search=m.toString();try{await wt(g.toString(),s,l)}catch(f){if(!Mt(f))throw P("SseFetchFailed",t,{method:e,url:n,error:f})}}finally{Q(Oe,s,{}),c()}};var Pt={type:3,name:"delete",fn:async(t,e,n)=>z(t,"DELETE",e,{...n})};var Ct={type:3,name:"get",fn:async(t,e,n)=>z(t,"GET",e,{...n})};var Nt={type:3,name:"patch",fn:async(t,e,n)=>z(t,"PATCH",e,{...n})};var kt={type:3,name:"post",fn:async(t,e,n)=>z(t,"POST",e,{...n})};var It={type:3,name:"put",fn:async(t,e,n)=>z(t,"PUT",e,{...n})};var Vt={type:1,name:"indicator",keyReq:3,valReq:3,onLoad:({el:t,key:e,mods:n,signals:r,value:i})=>{let o=e?H(e,n):Z(i),{signal:s}=r.upsertIfMissing(o,!1),a=p=>{let{type:y,elId:v}=p.detail;if(v===t.id)switch(y){case De:s.value=!0;break;case Oe:s.value=!1,document.removeEventListener(ie,a);break}};document.addEventListener(ie,a)}};var Lt={type:2,name:F.ExecuteScript,onGlobalInit:async t=>{J(F.ExecuteScript,({autoRemove:e=`${ot}`,attributes:n=rt,script:r})=>{let i=X(e);if(!r?.length)throw $("NoScriptProvided",t);let o=document.createElement("script");for(let s of n.split(` 11 | `)){let a=s.indexOf(" "),p=a?s.slice(0,a):s,y=a?s.slice(a):"";o.setAttribute(p.trim(),y.trim())}o.text=r,document.head.appendChild(o),i&&o.remove()})}};var Ae=document,oe=!!Ae.startViewTransition;function W(t,e){if(e.has("viewtransition")&&oe){let n=t;t=(...r)=>document.startViewTransition(()=>n(...r))}return t}var Dt=function(){"use strict";let t=()=>{},e={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:t,afterNodeAdded:t,beforeNodeMorphed:t,afterNodeMorphed:t,beforeNodeRemoved:t,afterNodeRemoved:t,beforeAttributeUpdated:t},head:{style:"merge",shouldPreserve:b=>b.getAttribute("im-preserve")==="true",shouldReAppend:b=>b.getAttribute("im-re-append")==="true",shouldRemove:t,afterHeadMorphed:t},restoreFocus:!0};function n(b,A,h={}){b=v(b);let T=k(A),_=y(b,T,h),E=i(_,()=>a(_,b,T,c=>c.morphStyle==="innerHTML"?(o(c,b,T),Array.from(b.childNodes)):r(c,b,T)));return _.pantry.remove(),E}function r(b,A,h){let T=k(A);return o(b,T,h,A,A.nextSibling),Array.from(T.childNodes)}function i(b,A){if(!b.config.restoreFocus)return A();let h=document.activeElement;if(!(h instanceof HTMLInputElement||h instanceof HTMLTextAreaElement))return A();let{id:T,selectionStart:_,selectionEnd:E}=h,c=A();return T&&T!==document.activeElement?.id&&(h=b.target.querySelector(`[id="${T}"]`),h?.focus()),h&&!h.selectionEnd&&E&&h.setSelectionRange(_,E),c}let o=function(){function b(u,l,g,m=null,f=null){l instanceof HTMLTemplateElement&&g instanceof HTMLTemplateElement&&(l=l.content,g=g.content),m||=l.firstChild;for(let S of g.childNodes){if(m&&m!=f){let R=h(u,S,m,f);if(R){R!==m&&_(u,m,R),s(R,S,u),m=R.nextSibling;continue}}if(S instanceof Element&&u.persistentIds.has(S.id)){let R=E(l,S.id,m,u);s(R,S,u),m=R.nextSibling;continue}let w=A(l,S,m,u);w&&(m=w.nextSibling)}for(;m&&m!=f;){let S=m;m=m.nextSibling,T(u,S)}}function A(u,l,g,m){if(m.callbacks.beforeNodeAdded(l)===!1)return null;if(m.idMap.has(l)){let f=document.createElement(l.tagName);return u.insertBefore(f,g),s(f,l,m),m.callbacks.afterNodeAdded(f),f}else{let f=document.importNode(l,!0);return u.insertBefore(f,g),m.callbacks.afterNodeAdded(f),f}}let h=function(){function u(m,f,S,w){let R=null,M=f.nextSibling,L=0,N=S;for(;N&&N!=w;){if(g(N,f)){if(l(m,N,f))return N;R===null&&(m.idMap.has(N)||(R=N))}if(R===null&&M&&g(N,M)&&(L++,M=M.nextSibling,L>=2&&(R=void 0)),N.contains(document.activeElement))break;N=N.nextSibling}return R||null}function l(m,f,S){let w=m.idMap.get(f),R=m.idMap.get(S);if(!R||!w)return!1;for(let M of w)if(R.has(M))return!0;return!1}function g(m,f){let S=m,w=f;return S.nodeType===w.nodeType&&S.tagName===w.tagName&&(!S.id||S.id===w.id)}return u}();function T(u,l){if(u.idMap.has(l))d(u.pantry,l,null);else{if(u.callbacks.beforeNodeRemoved(l)===!1)return;l.parentNode?.removeChild(l),u.callbacks.afterNodeRemoved(l)}}function _(u,l,g){let m=l;for(;m&&m!==g;){let f=m;m=m.nextSibling,T(u,f)}return m}function E(u,l,g,m){let f=m.target.id===l&&m.target||m.target.querySelector(`[id="${l}"]`)||m.pantry.querySelector(`[id="${l}"]`);return c(f,m),d(u,f,g),f}function c(u,l){let g=u.id;for(;u=u.parentNode;){let m=l.idMap.get(u);m&&(m.delete(g),m.size||l.idMap.delete(u))}}function d(u,l,g){if(u.moveBefore)try{u.moveBefore(l,g)}catch{u.insertBefore(l,g)}else u.insertBefore(l,g)}return b}(),s=function(){function b(c,d,u){return u.ignoreActive&&c===document.activeElement?null:(u.callbacks.beforeNodeMorphed(c,d)===!1||(c instanceof HTMLHeadElement&&u.head.ignore||(c instanceof HTMLHeadElement&&u.head.style!=="morph"?p(c,d,u):(A(c,d,u),E(c,u)||o(u,c,d))),u.callbacks.afterNodeMorphed(c,d)),c)}function A(c,d,u){let l=d.nodeType;if(l===1){let g=c,m=d,f=g.attributes,S=m.attributes;for(let w of S)_(w.name,g,"update",u)||g.getAttribute(w.name)!==w.value&&g.setAttribute(w.name,w.value);for(let w=f.length-1;0<=w;w--){let R=f[w];if(R&&!m.hasAttribute(R.name)){if(_(R.name,g,"remove",u))continue;g.removeAttribute(R.name)}}E(g,u)||h(g,m,u)}(l===8||l===3)&&c.nodeValue!==d.nodeValue&&(c.nodeValue=d.nodeValue)}function h(c,d,u){if(c instanceof HTMLInputElement&&d instanceof HTMLInputElement&&d.type!=="file"){let l=d.value,g=c.value;T(c,d,"checked",u),T(c,d,"disabled",u),d.hasAttribute("value")?g!==l&&(_("value",c,"update",u)||(c.setAttribute("value",l),c.value=l)):_("value",c,"remove",u)||(c.value="",c.removeAttribute("value"))}else if(c instanceof HTMLOptionElement&&d instanceof HTMLOptionElement)T(c,d,"selected",u);else if(c instanceof HTMLTextAreaElement&&d instanceof HTMLTextAreaElement){let l=d.value,g=c.value;if(_("value",c,"update",u))return;l!==g&&(c.value=l),c.firstChild&&c.firstChild.nodeValue!==l&&(c.firstChild.nodeValue=l)}}function T(c,d,u,l){let g=d[u],m=c[u];if(g!==m){let f=_(u,c,"update",l);f||(c[u]=d[u]),g?f||c.setAttribute(u,""):_(u,c,"remove",l)||c.removeAttribute(u)}}function _(c,d,u,l){return c==="value"&&l.ignoreActiveValue&&d===document.activeElement?!0:l.callbacks.beforeAttributeUpdated(c,d,u)===!1}function E(c,d){return!!d.ignoreActiveValue&&c===document.activeElement&&c!==document.body}return b}();function a(b,A,h,T){if(b.head.block){let _=A.querySelector("head"),E=h.querySelector("head");if(_&&E){let c=p(_,E,b);return Promise.all(c).then(()=>{let d=Object.assign(b,{head:{block:!1,ignore:!0}});return T(d)})}}return T(b)}function p(b,A,h){let T=[],_=[],E=[],c=[],d=new Map;for(let l of A.children)d.set(l.outerHTML,l);for(let l of b.children){let g=d.has(l.outerHTML),m=h.head.shouldReAppend(l),f=h.head.shouldPreserve(l);g||f?m?_.push(l):(d.delete(l.outerHTML),E.push(l)):h.head.style==="append"?m&&(_.push(l),c.push(l)):h.head.shouldRemove(l)!==!1&&_.push(l)}c.push(...d.values());let u=[];for(let l of c){let g=document.createRange().createContextualFragment(l.outerHTML).firstChild;if(h.callbacks.beforeNodeAdded(g)!==!1){if("href"in g&&g.href||"src"in g&&g.src){let m,f=new Promise(function(S){m=S});g.addEventListener("load",function(){m()}),u.push(f)}b.appendChild(g),h.callbacks.afterNodeAdded(g),T.push(g)}}for(let l of _)h.callbacks.beforeNodeRemoved(l)!==!1&&(b.removeChild(l),h.callbacks.afterNodeRemoved(l));return h.head.afterHeadMorphed(b,{added:T,kept:E,removed:_}),u}let y=function(){function b(d,u,l){let{persistentIds:g,idMap:m}=E(d,u),f=A(l),S=f.morphStyle||"outerHTML";if(!["innerHTML","outerHTML"].includes(S))throw`Do not understand how to morph style ${S}`;return{target:d,newContent:u,config:f,morphStyle:S,ignoreActive:f.ignoreActive,ignoreActiveValue:f.ignoreActiveValue,restoreFocus:f.restoreFocus,idMap:m,persistentIds:g,pantry:h(),callbacks:f.callbacks,head:f.head}}function A(d){let u=Object.assign({},e);return Object.assign(u,d),u.callbacks=Object.assign({},e.callbacks,d.callbacks),u.head=Object.assign({},e.head,d.head),u}function h(){let d=document.createElement("div");return d.hidden=!0,document.body.insertAdjacentElement("afterend",d),d}function T(d){let u=Array.from(d.querySelectorAll("[id]"));return d.id&&u.push(d),u}function _(d,u,l,g){for(let m of g)if(u.has(m.id)){let f=m;for(;f;){let S=d.get(f);if(S==null&&(S=new Set,d.set(f,S)),S.add(m.id),f===l)break;f=f.parentElement}}}function E(d,u){let l=T(d),g=T(u),m=c(l,g),f=new Map;_(f,m,d,l);let S=u.__idiomorphRoot||u;return _(f,m,S,g),{persistentIds:m,idMap:f}}function c(d,u){let l=new Set,g=new Map;for(let{id:f,tagName:S}of d)g.has(f)?l.add(f):g.set(f,S);let m=new Set;for(let{id:f,tagName:S}of u)m.has(f)?l.add(f):g.get(f)===S&&m.add(f);for(let f of l)m.delete(f);return m}return b}(),{normalizeElement:v,normalizeParent:k}=function(){let b=new WeakSet;function A(E){return E instanceof Document?E.documentElement:E}function h(E){if(E==null)return document.createElement("div");if(typeof E=="string")return h(_(E));if(b.has(E))return E;if(E instanceof Node){if(E.parentNode)return new T(E);{let c=document.createElement("div");return c.append(E),c}}else{let c=document.createElement("div");for(let d of[...E])c.append(d);return c}}class T{constructor(c){this.originalNode=c,this.realParentNode=c.parentNode,this.previousSibling=c.previousSibling,this.nextSibling=c.nextSibling}get childNodes(){let c=[],d=this.previousSibling?this.previousSibling.nextSibling:this.realParentNode.firstChild;for(;d&&d!=this.nextSibling;)c.push(d),d=d.nextSibling;return c}querySelectorAll(c){return this.childNodes.reduce((d,u)=>{if(u instanceof Element){u.matches(c)&&d.push(u);let l=u.querySelectorAll(c);for(let g=0;g]*>|>)([\s\S]*?)<\/svg>/gim,"");if(d.match(/<\/html>/)||d.match(/<\/head>/)||d.match(/<\/body>/)){let u=c.parseFromString(E,"text/html");if(d.match(/<\/html>/))return b.add(u),u;{let l=u.firstChild;return l&&b.add(l),l}}else{let l=c.parseFromString("","text/html").body.querySelector("template").content;return b.add(l),l}}return{normalizeElement:A,normalizeParent:h}}();return{morph:n,defaults:e}}();var Ht={type:2,name:F.MergeFragments,onGlobalInit:async t=>{let e=document.createElement("template");J(F.MergeFragments,({fragments:n="
",selector:r="",mergeMode:i=st,useViewTransition:o=`${Re}`})=>{let s=X(o);e.innerHTML=n.trim();let a=[...e.content.children];for(let p of a){if(!(p instanceof Element))throw $("NoFragmentsFound",t);let y=r||`#${p.getAttribute("id")}`,v=[...document.querySelectorAll(y)||[]];if(!v.length)throw $("NoTargetsFound",t,{selectorOrID:y});s&&oe?Ae.startViewTransition(()=>Ot(t,i,p,v)):Ot(t,i,p,v)}})}};function Ot(t,e,n,r){for(let i of r){i.dataset.fragmentMergeTarget="true";let o=n.cloneNode(!0);switch(e){case U.Morph:{be(o,s=>{!s.id?.length&&Object.keys(s.dataset).length&&(s.id=we(s));let a=t.removals.get(s.id);if(a){let p=new Map;for(let[y,v]of a){let k=Me(y,y);p.set(k,v),a.delete(y)}t.removals.set(s.id,p)}}),Dt.morph(i,o);break}case U.Inner:i.innerHTML=o.outerHTML;break;case U.Outer:i.replaceWith(o);break;case U.Prepend:i.prepend(o);break;case U.Append:i.append(o);break;case U.Before:i.before(o);break;case U.After:i.after(o);break;case U.UpsertAttributes:for(let s of o.getAttributeNames()){let a=o.getAttribute(s);i.setAttribute(s,a)}break;default:throw $("InvalidMergeMode",t,{mergeMode:e})}}}var Ft={type:2,name:F.MergeSignals,onGlobalInit:async t=>{J(F.MergeSignals,({signals:e="{}",onlyIfMissing:n=`${it}`})=>{let{signals:r}=t,i=X(n);r.merge(xe(e),i)})}};var qt={type:2,name:F.RemoveFragments,onGlobalInit:async t=>{J(F.RemoveFragments,({selector:e,useViewTransition:n=`${Re}`})=>{if(!e.length)throw $("NoSelectorProvided",t);let r=X(n),i=document.querySelectorAll(e),o=()=>{for(let s of i)s.remove()};r&&oe?Ae.startViewTransition(()=>o()):o()})}};var $t={type:2,name:F.RemoveSignals,onGlobalInit:async t=>{J(F.RemoveSignals,({paths:e=""})=>{let n=e.split(` 12 | `).map(r=>r.trim());if(!n?.length)throw $("NoPathsProvided",t);t.signals.remove(...n)})}};var Wt={type:3,name:"clipboard",fn:(t,e)=>{if(!navigator.clipboard)throw P("ClipboardNotAvailable",t);navigator.clipboard.writeText(e)}};var Bt={type:1,name:"customValidity",keyReq:2,valReq:1,onLoad:t=>{let{el:e,genRX:n,effect:r}=t;if(!(e instanceof HTMLInputElement||e instanceof HTMLSelectElement||e instanceof HTMLTextAreaElement))throw P("CustomValidityInvalidElement",t);let i=n();return r(()=>{let o=i();if(typeof o!="string")throw P("CustomValidityInvalidExpression",t,{result:o});e.setCustomValidity(o)})}};function se(t){if(!t||t.size<=0)return 0;for(let e of t){if(e.endsWith("ms"))return Number(e.replace("ms",""));if(e.endsWith("s"))return Number(e.replace("s",""))*1e3;try{return Number.parseFloat(e)}catch{}}return 0}function ae(t,e,n=!1){return t?t.has(e.toLowerCase()):n}function Ln(t,e,n=!1,r=!0){let i=-1,o=()=>i&&clearTimeout(i);return(...s)=>{o(),n&&!i&&t(...s),i=setTimeout(()=>{r&&t(...s),o()},e)}}function Dn(t,e,n=!0,r=!1){let i=!1;return(...o)=>{i||(n&&t(...o),i=!0,setTimeout(()=>{i=!1,r&&t(...o)},e))}}function ee(t,e){let n=e.get("debounce");if(n){let i=se(n),o=ae(n,"leading",!1),s=!ae(n,"notrail",!1);t=Ln(t,i,o,s)}let r=e.get("throttle");if(r){let i=se(r),o=!ae(r,"noleading",!1),s=ae(r,"trail",!1);t=Dn(t,i,o,s)}return t}var Gt={type:1,name:"onIntersect",keyReq:2,onLoad:({el:t,rawKey:e,mods:n,genRX:r})=>{let i=ee(r(),n);i=W(i,n);let o={threshold:0};n.has("full")?o.threshold=1:n.has("half")&&(o.threshold=.5);let s=new IntersectionObserver(a=>{for(let p of a)p.isIntersecting&&(i(),n.has("once")&&(s.disconnect(),delete t.dataset[e]))},o);return s.observe(t),()=>s.disconnect()}};var Ut={type:1,name:"onInterval",keyReq:2,valReq:1,onLoad:({mods:t,genRX:e})=>{let n=W(e(),t),r=1e3,i=t.get("duration");i&&(r=se(i),ae(i,"leading",!1)&&n());let o=setInterval(n,r);return()=>{clearInterval(o)}}};var jt={type:1,name:"onLoad",keyReq:2,valReq:1,onLoad:({mods:t,genRX:e})=>{let n=W(e(),t),r=0,i=t.get("delay");return i&&(r=se(i)),setTimeout(n,r),()=>{}}};var Kt={type:1,name:"onRaf",keyReq:2,valReq:1,onLoad:({mods:t,genRX:e})=>{let n=ee(e(),t);n=W(n,t);let r,i=()=>{n(),r=requestAnimationFrame(i)};return r=requestAnimationFrame(i),()=>{r&&cancelAnimationFrame(r)}}};function Ye(t,e){return e=e.replaceAll(".","\\.").replaceAll("**",le).replaceAll("*","[^\\.]*").replaceAll(le,".*"),new RegExp(`^${e}$`).test(t)}function he(t,e){let n=[],r=e.split(/\s+/).filter(i=>i!=="");r=r.map(i=>Z(i));for(let i of r)t.walk(o=>{Ye(o,i)&&n.push(o)});return n}var Jt={type:1,name:"onSignalChange",valReq:1,onLoad:({key:t,mods:e,signals:n,genRX:r})=>{let i=ee(r(),e);if(i=W(i,e),t===""){let a=p=>i(p);return document.addEventListener(ye,a),()=>{document.removeEventListener(ye,a)}}let o=H(t,e),s=new Map;return n.walk((a,p)=>{Ye(a,o)&&s.set(p,p.value)}),me(()=>{for(let[a,p]of s)p!==a.value&&(i(),s.set(a,a.value))})}};var zt={type:1,name:"persist",keyReq:2,onLoad:({effect:t,mods:e,signals:n,value:r})=>{let i=q,o=e.has("session")?sessionStorage:localStorage,s=r!==""?r:"**",a=()=>{let y=o.getItem(i)||"{}",v=JSON.parse(y);n.merge(v)},p=()=>{let y=he(n,s),v=n.subset(...y);o.setItem(i,JSON.stringify(v))};return a(),t(()=>{p()})}};var Yt={type:1,name:"replaceUrl",keyReq:2,valReq:1,onLoad:({effect:t,genRX:e})=>{let n=e();return t(()=>{let r=n(),i=window.location.href,o=new URL(r,i).toString();window.history.replaceState({},"",o)})}};var Xe="smooth",Xt="instant",Zt="auto",On="hstart",Hn="hcenter",Fn="hend",qn="hnearest",$n="vstart",Wn="vcenter",Bn="vend",Gn="vnearest",He="center",Qt="start",en="end",tn="nearest",Un="focus",nn={type:1,name:"scrollIntoView",keyReq:2,valReq:2,onLoad:t=>{let{el:e,mods:n,rawKey:r}=t;e.tabIndex||e.setAttribute("tabindex","0");let i={behavior:Xe,block:He,inline:He};if(n.has(Xe)&&(i.behavior=Xe),n.has(Xt)&&(i.behavior=Xt),n.has(Zt)&&(i.behavior=Zt),n.has(On)&&(i.inline=Qt),n.has(Hn)&&(i.inline=He),n.has(Fn)&&(i.inline=en),n.has(qn)&&(i.inline=tn),n.has($n)&&(i.block=Qt),n.has(Wn)&&(i.block=He),n.has(Bn)&&(i.block=en),n.has(Gn)&&(i.block=tn),!(e instanceof HTMLElement||e instanceof SVGElement))throw P("ScrollIntoViewInvalidElement",t);e.tabIndex||e.setAttribute("tabindex","0"),e.scrollIntoView(i),n.has(Un)&&e.focus(),delete e.dataset[r]}};var rn="view-transition",on={type:1,name:"viewTransition",keyReq:2,valReq:1,onGlobalInit(){let t=!1;for(let e of document.head.childNodes)e instanceof HTMLMetaElement&&e.name===rn&&(t=!0);if(!t){let e=document.createElement("meta");e.name=rn,e.content="same-origin",document.head.appendChild(e)}},onLoad:({effect:t,el:e,genRX:n})=>{if(!oe){console.error("Browser does not support view transitions");return}let r=n();return t(()=>{let i=r();if(!i?.length)return;let o=e.style;o.viewTransitionName=i})}};var sn={type:1,name:"attr",valReq:1,onLoad:({el:t,key:e,effect:n,genRX:r})=>{let i=r();return e===""?n(async()=>{let o=i();for(let[s,a]of Object.entries(o))a===!1?t.removeAttribute(s):t.setAttribute(s,a)}):(e=Y(e),n(async()=>{let o=!1;try{o=i()}catch{}let s;typeof o=="string"?s=o:s=JSON.stringify(o),!s||s==="false"||s==="null"||s==="undefined"?t.removeAttribute(e):t.setAttribute(e,s)}))}};var jn=/^data:(?[^;]+);base64,(?.*)$/,an=["change","input","keydown"],ln={type:1,name:"bind",keyReq:3,valReq:3,onLoad:t=>{let{el:e,key:n,mods:r,signals:i,value:o,effect:s}=t,a=e,p=n?H(n,r):Z(o),y=e.tagName.toLowerCase(),v=y.includes("input"),k=y.includes("select"),b=e.getAttribute("type"),A=e.hasAttribute("value"),h="",T=v&&b==="checkbox";T&&(h=A?"":!1);let _=v&&b==="number";_&&(h=0);let E=v&&b==="radio";E&&(e.getAttribute("name")?.length||e.setAttribute("name",p));let c=v&&b==="file",{signal:d,inserted:u}=i.upsertIfMissing(p,h),l=-1;Array.isArray(d.value)&&(e.getAttribute("name")===null&&e.setAttribute("name",p),l=[...document.querySelectorAll(`[name="${p}"]`)].findIndex(M=>M===t.el));let g=l>=0,m=()=>[...i.value(p)],f=()=>{let M=i.value(p);g&&!k&&(M=M[l]||h);let L=`${M}`;if(T||E)typeof M=="boolean"?a.checked=M:a.checked=L===a.value;else if(k){let N=e;if(N.multiple){if(!g)throw P("BindSelectMultiple",t);for(let O of N.options){if(O?.disabled)return;let D=_?Number(O.value):O.value;O.selected=M.includes(D)}}else N.value=L}else c||("value"in e?e.value=L:e.setAttribute("value",L))},S=async()=>{let M=i.value(p);if(g){let D=M;for(;l>=D.length;)D.push(h);M=D[l]||h}let L=(D,B)=>{let G=B;g&&!k&&(G=m(),G[l]=B),i.setValue(D,G)};if(c){let D=[...a?.files||[]],B=[],G=[],Ze=[];await Promise.all(D.map(Qe=>new Promise(bn=>{let te=new FileReader;te.onload=()=>{if(typeof te.result!="string")throw P("InvalidFileResultType",t,{resultType:typeof te.result});let Fe=te.result.match(jn);if(!Fe?.groups)throw P("InvalidDataUri",t,{result:te.result});B.push(Fe.groups.contents),G.push(Fe.groups.mime),Ze.push(Qe.name)},te.onloadend=()=>bn(void 0),te.readAsDataURL(Qe)}))),L(p,B),L(`${p}Mimes`,G),L(`${p}Names`,Ze);return}let N=a.value||"",O;if(T){let D=a.checked||a.getAttribute("checked")==="true";A?O=D?N:"":O=D}else if(k){let B=[...e.selectedOptions];g?O=B.filter(G=>G.selected).map(G=>G.value):O=B[0]?.value||h}else typeof M=="boolean"?O=!!N:typeof M=="number"?O=Number(N):O=N||"";L(p,O)};u&&S();for(let M of an)e.addEventListener(M,S);let w=M=>{M.persisted&&S()};window.addEventListener("pageshow",w);let R=s(()=>f());return()=>{R();for(let M of an)e.removeEventListener(M,S);window.removeEventListener("pageshow",w)}}};var un={type:1,name:"class",valReq:1,onLoad:({el:t,key:e,mods:n,effect:r,genRX:i})=>{let o=t.classList,s=i();return r(()=>{if(e===""){let a=s();for(let[p,y]of Object.entries(a)){let v=p.split(/\s+/);y?o.add(...v):o.remove(...v)}}else{let a=Y(e);a=H(a,n),s()?o.add(a):o.remove(a)}})}};var cn={type:1,name:"on",keyReq:1,valReq:1,argNames:["evt"],onLoad:({el:t,key:e,mods:n,genRX:r})=>{let i=r(),o=t;n.has("window")&&(o=window);let s=v=>{v&&((n.has("prevent")||e==="submit")&&v.preventDefault(),n.has("stop")&&v.stopPropagation()),i(v)};s=ee(s,n),s=W(s,n);let a={capture:!1,passive:!1,once:!1};if(n.has("capture")&&(a.capture=!0),n.has("passive")&&(a.passive=!0),n.has("once")&&(a.once=!0),n.has("outside")){o=document;let v=s;s=b=>{let A=b?.target;t.contains(A)||v(b)}}let y=Y(e);return y=H(y,n),y===ie&&(o=document),o.addEventListener(y,s,a),()=>{o.removeEventListener(y,s)}}};var fn={type:1,name:"ref",keyReq:3,valReq:3,onLoad:({el:t,key:e,mods:n,signals:r,value:i})=>{let o=e?H(e,n):Z(i);r.setValue(o,t)}};var dn="none",pn="display",mn={type:1,name:"show",keyReq:2,valReq:1,onLoad:({el:{style:t},genRX:e,effect:n})=>{let r=e();return n(async()=>{r()?t.display===dn&&t.removeProperty(pn):t.setProperty(pn,dn)})}};var gn={type:1,name:"text",keyReq:2,valReq:1,onLoad:t=>{let{el:e,effect:n,genRX:r}=t,i=r();return e instanceof HTMLElement||P("TextInvalidElement",t),n(()=>{let o=i(t);e.textContent=`${o}`})}};var{round:Kn,max:Jn,min:zn}=Math,hn={type:3,name:"fit",fn:(t,e,n,r,i,o,s=!1,a=!1)=>{let p=(e-n)/(r-n)*(o-i)+i;return a&&(p=Kn(p)),s&&(p=Jn(i,zn(o,p))),p}};var yn={type:3,name:"setAll",fn:({signals:t},e,n)=>{let r=he(t,e);for(let i of r)t.setValue(i,n)}};var vn={type:3,name:"toggleAll",fn:({signals:t},e)=>{let n=he(t,e);for(let r of n)t.setValue(r,!t.value(r))}};Ve(sn,ln,un,cn,fn,mn,gn,Vt,Ct,kt,It,Nt,Pt,Ht,Ft,qt,$t,Lt,Wt,Bt,Gt,Ut,jt,Kt,Jt,zt,Yt,nn,on,hn,yn,vn);ze();export{ze as apply,Ve as load,Et as setAlias}; 13 | //# sourceMappingURL=datastar.js.map 14 | -------------------------------------------------------------------------------- /src/DatastarEventStream.php: -------------------------------------------------------------------------------- 1 | getStreamedResponse($callable); 22 | } 23 | 24 | /** 25 | * Returns a signals model populated with signals passed into the request. 26 | */ 27 | protected function getSignals(): Signals 28 | { 29 | return app(Sse::class)->getSignals(); 30 | } 31 | 32 | /** 33 | * Merges HTML fragments into the DOM. 34 | */ 35 | protected function mergeFragments(string $data, array $options = []): void 36 | { 37 | app(Sse::class)->mergeFragments($data, $options); 38 | } 39 | 40 | /** 41 | * Removes HTML fragments from the DOM. 42 | */ 43 | protected function removeFragments(string $selector, array $options = []): void 44 | { 45 | app(Sse::class)->removeFragments($selector, $options); 46 | } 47 | 48 | /** 49 | * Merges signals. 50 | */ 51 | protected function mergeSignals(array $signals, array $options = []): void 52 | { 53 | app(Sse::class)->mergeSignals($signals, $options); 54 | } 55 | 56 | /** 57 | * Removes signal paths. 58 | */ 59 | protected function removeSignals(array $paths, array $options = []): void 60 | { 61 | app(Sse::class)->removeSignals($paths, $options); 62 | } 63 | 64 | /** 65 | * Executes JavaScript in the browser. 66 | */ 67 | protected function executeScript(string $script, array $options = []): void 68 | { 69 | app(Sse::class)->executeScript($script, $options); 70 | } 71 | 72 | /** 73 | * Redirects the browser by setting the location to the provided URI. 74 | */ 75 | protected function location(string $uri, array $options = []): void 76 | { 77 | app(Sse::class)->location($uri, $options); 78 | } 79 | 80 | /** 81 | * Renders and returns Datastar view. 82 | */ 83 | protected function renderDatastarView(string $view, array $variables = []): string 84 | { 85 | return app(Sse::class)->renderDatastarView($view, $variables); 86 | } 87 | 88 | /** 89 | * Throws an exception with the appropriate formats for easier debugging. 90 | * 91 | * @phpstan-return never 92 | */ 93 | protected function throwException(Throwable|string $exception): void 94 | { 95 | app(Sse::class)->throwException($exception); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/DatastarServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/../config/datastar.php', 'datastar'); 20 | 21 | $this->app->singleton(Sse::class, function() { 22 | return new Sse(); 23 | }); 24 | } 25 | 26 | public function boot(): void 27 | { 28 | $this->publishes([ 29 | __DIR__ . '/../config/datastar.php' => config_path('datastar.php'), 30 | ], 'datastar-config'); 31 | 32 | $this->publishes([ 33 | __DIR__ . '/../public' => public_path('vendor'), 34 | ], 'public'); 35 | 36 | $this->registerRoutes(); 37 | $this->registerScript(); 38 | $this->registerDirectives(); 39 | } 40 | 41 | private function registerRoutes(): void 42 | { 43 | Route::middleware(['web'])->group(function() { 44 | Route::any( 45 | '/datastar-controller', 46 | [DatastarController::class, 'index'], 47 | ); 48 | }); 49 | } 50 | 51 | private function registerScript(): void 52 | { 53 | if (config('datastar.registerScript', true) === false) { 54 | return; 55 | } 56 | 57 | $this->app['router']->pushMiddlewareToGroup('web', RegisterScript::class); 58 | } 59 | 60 | /** 61 | * @uses Sse::mergeFragments() 62 | * @uses Sse::removeFragments() 63 | * @uses Sse::mergeSignals() 64 | * @uses Sse::removeSignals() 65 | * @uses Sse::executeScript() 66 | * @uses Sse::location() 67 | * @uses Sse::setSseInProcess 68 | */ 69 | private function registerDirectives(): void 70 | { 71 | Blade::directive('mergefragments', function(string $expression) { 72 | return $this->getDirective("setSseInProcess('mergeFragments', $expression); ob_start()"); 73 | }); 74 | 75 | Blade::directive('endmergefragments', function() { 76 | return $this->getDirective("mergeFragments(ob_get_clean())"); 77 | }); 78 | 79 | Blade::directive('removefragments', function(string $expression) { 80 | return $this->getDirective("removeFragments($expression)"); 81 | }); 82 | 83 | Blade::directive('mergesignals', function(string $expression) { 84 | return $this->getDirective("mergeSignals($expression)"); 85 | }); 86 | 87 | Blade::directive('removesignals', function(string $expression) { 88 | return $this->getDirective("removeSignals($expression)"); 89 | }); 90 | 91 | Blade::directive('executescript', function(string $expression) { 92 | return $this->getDirective("setSseInProcess('executeScript', $expression); ob_start()"); 93 | }); 94 | 95 | Blade::directive('endexecutescript', function() { 96 | return $this->getDirective("executeScript(ob_get_clean())"); 97 | }); 98 | 99 | Blade::directive('location', function(string $expression) { 100 | return $this->getDirective("location($expression)"); 101 | }); 102 | } 103 | 104 | private function getDirective(string $expression): string 105 | { 106 | return "$expression ?>"; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Helpers/Datastar.php: -------------------------------------------------------------------------------- 1 | getAction('get', $view, $variables, $options); 21 | } 22 | 23 | /** 24 | * Returns a Datastar `@post` action. 25 | */ 26 | public function post(string $view, array $variables = [], array $options = []): string 27 | { 28 | return $this->getAction('post', $view, $variables, $options); 29 | } 30 | 31 | /** 32 | * Returns a Datastar `@put` action. 33 | */ 34 | public function put(string $view, array $variables = [], array $options = []): string 35 | { 36 | return $this->getAction('put', $view, $variables, $options); 37 | } 38 | 39 | /** 40 | * Returns a Datastar `@patch` action. 41 | */ 42 | public function patch(string $view, array $variables = [], array $options = []): string 43 | { 44 | return $this->getAction('patch', $view, $variables, $options); 45 | } 46 | 47 | /** 48 | * Returns a Datastar `@delete` action. 49 | */ 50 | public function delete(string $view, array $variables = [], array $options = []): string 51 | { 52 | return $this->getAction('delete', $view, $variables, $options); 53 | } 54 | 55 | /** 56 | * Returns a Datastar `@get` action that fetches and merges fragments. 57 | */ 58 | public function getFragments(string $view, array $variables = [], array $options = []): string 59 | { 60 | return $this->getAction('getFragments', $view, $variables, $options); 61 | } 62 | 63 | /** 64 | * Returns a Datastar action. 65 | */ 66 | private function getAction(string $method, string $view, array $variables, array $options): string 67 | { 68 | $config = new Config([ 69 | 'view' => $view, 70 | 'variables' => $variables, 71 | ]); 72 | 73 | if ($method === 'getFragments') { 74 | $config->getFragments = true; 75 | $method = 'get'; 76 | } 77 | 78 | try { 79 | $config->validate(); 80 | } catch (ValidationException $exception) { 81 | throw new BadRequestHttpException($exception->getMessage()); 82 | } 83 | 84 | $url = action( 85 | [DatastarController::class, 'index'], 86 | ['config' => $config->getHashed()], 87 | ); 88 | 89 | $args = ["'$url'"]; 90 | 91 | if ($method !== 'get') { 92 | $headers = $options['headers'] ?? []; 93 | $headers['X-CSRF-TOKEN'] = csrf_token(); 94 | $options['headers'] = $headers; 95 | } 96 | 97 | if (!empty($options)) { 98 | $args[] = json_encode($options); 99 | } 100 | 101 | $args = implode(', ', $args); 102 | 103 | return "@$method($args)"; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Http/Controllers/DatastarController.php: -------------------------------------------------------------------------------- 1 | getStreamedResponse(function() { 23 | $hashedConfig = request()->input('config'); 24 | $config = Config::fromHashed($hashedConfig); 25 | if ($config === null) { 26 | $this->throwException('Submitted data was tampered.'); 27 | } 28 | 29 | $output = $this->renderDatastarView($config->view, $config->variables); 30 | 31 | if ($config->getFragments) { 32 | $this->mergeFragments($output); 33 | } 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Http/Middleware/RegisterScript.php: -------------------------------------------------------------------------------- 1 | isSuccessful()) { 19 | return $response; 20 | } 21 | 22 | if (!str_contains($response->headers->get('content-type'), 'text/html')) { 23 | return $response; 24 | } 25 | 26 | $content = $response->getContent(); 27 | $path = asset('vendor/datastar/' . Consts::VERSION . '/datastar.js'); 28 | $asset = ''; 29 | $content = str_replace('', $asset . '', $content); 30 | $response->setContent($content); 31 | 32 | return $response; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/Config.php: -------------------------------------------------------------------------------- 1 | getKey(); 45 | 46 | return hash_hmac(self::HASH_ALGORITHM, $value, $key); 47 | } 48 | 49 | public function __construct(array $attributes = []) 50 | { 51 | foreach ($attributes as $key => $value) { 52 | $this->$key = $value; 53 | } 54 | } 55 | 56 | /** 57 | * Returns a hashed, JSON-encoded array of attributes. 58 | */ 59 | public function getHashed(): string 60 | { 61 | $attributes = array_filter([ 62 | 'view' => $this->view, 63 | 'variables' => $this->variables, 64 | 'getFragments' => $this->getFragments, 65 | ]); 66 | $encoded = json_encode($attributes); 67 | 68 | $checksum = self::hash($encoded); 69 | 70 | return $checksum . $encoded; 71 | } 72 | 73 | /** 74 | * Validates the model. 75 | */ 76 | public function validate(): void 77 | { 78 | $validator = Validator::make([ 79 | 'view' => $this->view, 80 | 'variables' => $this->variables, 81 | ], [ 82 | 'view' => 'required|string', 83 | 'variables' => function(string $attribute, mixed $variables, Closure $fail) { 84 | $this->validateVariables($attribute, $variables, $fail); 85 | }, 86 | ]); 87 | 88 | if ($validator->fails()) { 89 | throw new ValidationException($validator); 90 | } 91 | } 92 | 93 | /** 94 | * Validates that none of the variables are objects, recursively. 95 | */ 96 | private function validateVariables(string $attribute, mixed $variables, Closure $fail): void 97 | { 98 | $signalsVariableName = config('datastar.signalsVariableName'); 99 | 100 | foreach ($variables as $key => $value) { 101 | if ($key === $signalsVariableName) { 102 | $fail('Variable `' . $signalsVariableName . '` is reserved. Use a different name or modify the name of the signals variable using the `signalsVariableName` config setting.'); 103 | return; 104 | } 105 | 106 | if (is_object($value)) { 107 | $fail('Variable `' . $key . '` is an object, which is a forbidden variable type in the context of a Datastar request.'); 108 | return; 109 | } 110 | 111 | if (is_array($value)) { 112 | $this->validateVariables($attribute, $value, $fail); 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Models/Signals.php: -------------------------------------------------------------------------------- 1 | get($name); 20 | } 21 | 22 | /** 23 | * This exists so that `signals.{name}` and `signals.{name}({value})` will work in Twig. 24 | */ 25 | public function __call(string $name, array $arguments) 26 | { 27 | if (empty($arguments)) { 28 | return $this->get($name); 29 | } 30 | 31 | return $this->set($name, $arguments[0]); 32 | } 33 | 34 | /** 35 | * Returns the signal value, falling back to a default value. 36 | */ 37 | public function get(string $name, mixed $default = null): mixed 38 | { 39 | return $this->getNestedValue($name, $default); 40 | } 41 | 42 | /** 43 | * Returns the signal values. 44 | */ 45 | public function getValues(): array 46 | { 47 | return $this->values; 48 | } 49 | 50 | /** 51 | * Sets a signal value. 52 | */ 53 | public function set(string $name, mixed $value): static 54 | { 55 | $this->setNestedValue($name, $value); 56 | 57 | app(Sse::class)->mergeSignals($this->getNestedArrayValue($name, $value)); 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * Sets multiple signal values at once. 64 | */ 65 | public function setValues(array $values): static 66 | { 67 | foreach ($values as $name => $value) { 68 | $this->values[$name] = $value; 69 | } 70 | 71 | app(Sse::class)->mergeSignals($values); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Removes a signal. 78 | */ 79 | public function remove(string $name): static 80 | { 81 | $this->removeNestedValue($name); 82 | 83 | app(Sse::class)->removeSignals([$name]); 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Returns a nested value, falling back to a default value. 90 | */ 91 | private function getNestedValue(string $name, mixed $default = null): mixed 92 | { 93 | $parts = explode('.', $name); 94 | $current = &$this->values; 95 | foreach ($parts as $part) { 96 | if (!isset($current[$part])) { 97 | return $default; 98 | } 99 | $current = &$current[$part]; 100 | } 101 | 102 | return $current; 103 | } 104 | 105 | /** 106 | * Sets a nested signal value while supporting dot notation in the name. 107 | */ 108 | private function setNestedValue(string $name, mixed $value): void 109 | { 110 | $parts = explode('.', $name); 111 | $current = &$this->values; 112 | foreach ($parts as $part) { 113 | if (!isset($current[$part])) { 114 | $current[$part] = []; 115 | } 116 | $current = &$current[$part]; 117 | } 118 | $current = $value; 119 | } 120 | 121 | /** 122 | * Removes a nested signal value while supporting dot notation in the name. 123 | */ 124 | private function removeNestedValue(string $name): void 125 | { 126 | $parts = explode('.', $name); 127 | $part = reset($parts); 128 | $current = &$this->values; 129 | $parent = &$current; 130 | foreach ($parts as $part) { 131 | if (!isset($current[$part])) { 132 | return; 133 | } 134 | $parent = &$current; 135 | $current = &$current[$part]; 136 | } 137 | unset($parent[$part]); 138 | } 139 | 140 | /** 141 | * Returns a nested value while supporting dot notation in the name. 142 | */ 143 | private function getNestedArrayValue(string $name, mixed $value): array 144 | { 145 | $parts = explode('.', $name); 146 | $nestedValue = []; 147 | $current = &$nestedValue; 148 | foreach ($parts as $part) { 149 | $current[$part] = []; 150 | $current = &$current[$part]; 151 | } 152 | $current = $value; 153 | 154 | return $nestedValue; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Services/Sse.php: -------------------------------------------------------------------------------- 1 | $value) { 40 | $response->headers->set($name, $value); 41 | } 42 | 43 | return $response; 44 | } 45 | 46 | /** 47 | * Returns a signals model populated with signals passed into the request. 48 | */ 49 | public function getSignals(): Signals 50 | { 51 | return new Signals(ServerSentEventGenerator::readSignals()); 52 | } 53 | 54 | /** 55 | * Merges HTML fragments into the DOM. 56 | */ 57 | public function mergeFragments(string $data, array $options = []): void 58 | { 59 | $options = $this->mergeEventOptions( 60 | config('datastar.defaultFragmentOptions', []), 61 | $options, 62 | ); 63 | 64 | $this->sendSseEvent('mergeFragments', $data, $options); 65 | } 66 | 67 | /** 68 | * Removes HTML fragments from the DOM. 69 | */ 70 | public function removeFragments(string $selector, array $options = []): void 71 | { 72 | $options = $this->mergeEventOptions( 73 | config('datastar.defaultFragmentOptions', []), 74 | $options, 75 | ); 76 | 77 | $this->sendSseEvent('removeFragments', $selector, $options); 78 | } 79 | 80 | /** 81 | * Merges signals. 82 | */ 83 | public function mergeSignals(array $signals, array $options = []): void 84 | { 85 | $options = $this->mergeEventOptions( 86 | config('datastar.defaultSignalOptions', []), 87 | $options, 88 | ); 89 | 90 | $this->sendSseEvent('mergeSignals', $signals, $options); 91 | } 92 | 93 | /** 94 | * Removes signal paths. 95 | */ 96 | public function removeSignals(array $paths, array $options = []): void 97 | { 98 | $this->sendSseEvent('removeSignals', $paths, $options); 99 | } 100 | 101 | /** 102 | * Executes JavaScript in the browser. 103 | */ 104 | public function executeScript(string $script, array $options = []): void 105 | { 106 | $options = $this->mergeEventOptions( 107 | config('datastar.defaultExecuteScriptOptions', []), 108 | $options, 109 | ); 110 | 111 | $this->sendSseEvent('executeScript', $script, $options); 112 | } 113 | 114 | /** 115 | * Redirects the browser by setting the location to the provided URI. 116 | */ 117 | public function location(string $uri, array $options = []): void 118 | { 119 | $options = $this->mergeEventOptions( 120 | config('datastar.defaultExecuteScriptOptions', []), 121 | $options, 122 | ); 123 | 124 | $this->sendSseEvent('location', $uri, $options); 125 | } 126 | 127 | /** 128 | * Returns a rendered Datastar view. 129 | */ 130 | public function renderDatastarView(string $view, array $variables = []): string 131 | { 132 | if (!View::exists($view)) { 133 | $this->throwException('View `' . $view . '` does not exist.'); 134 | } 135 | 136 | $signals = $this->getSignals(); 137 | $variables = array_merge( 138 | [config('datastar.signalsVariableName', 'signals') => $signals], 139 | $variables, 140 | ); 141 | 142 | if (strtolower(request()->header('Content-Type')) === 'application/json') { 143 | // Clear out params to prevent them from being processed by controller actions. 144 | request()->query->replace(); 145 | request()->request->replace(); 146 | } 147 | 148 | try { 149 | $output = view($view, $variables)->render(); 150 | } catch (Throwable $exception) { 151 | $this->throwException($exception); 152 | } 153 | 154 | return $output; 155 | } 156 | 157 | /** 158 | * Sets the server sent event method and options currently in process. 159 | */ 160 | public function setSseInProcess(string $method, array $options = []): void 161 | { 162 | $this->sseMethodInProcess = $method; 163 | $this->sseOptionsInProcess = $options; 164 | } 165 | 166 | /** 167 | * Throws an exception with the appropriate formats for easier debugging. 168 | * 169 | * @phpstan-return never 170 | */ 171 | public function throwException(Throwable|string $exception): void 172 | { 173 | request()->headers->set('Accept', 'text/html'); 174 | 175 | if ($exception instanceof Throwable) { 176 | throw $exception; 177 | } 178 | 179 | throw new BadRequestHttpException($exception); 180 | } 181 | 182 | /** 183 | * Returns merged event options with null values removed. 184 | */ 185 | private function mergeEventOptions(array ...$optionSets): array 186 | { 187 | $options = array_merge( 188 | config('datastar.defaultEventOptions', []), 189 | $this->sseOptionsInProcess, 190 | ); 191 | 192 | $this->sseOptionsInProcess = []; 193 | 194 | foreach ($optionSets as $optionSet) { 195 | $options = array_merge($options, $optionSet); 196 | } 197 | 198 | return array_filter($options, fn($value) => $value !== null); 199 | } 200 | 201 | /** 202 | * Returns a server sent event generator. 203 | */ 204 | private function getSseGenerator(): ServerSentEventGenerator 205 | { 206 | if ($this->sseGenerator === null) { 207 | $this->sseGenerator = new ServerSentEventGenerator(); 208 | } 209 | 210 | return $this->sseGenerator; 211 | } 212 | 213 | /** 214 | * Sends an SSE event with arguments and cleans output buffers. 215 | */ 216 | private function sendSseEvent(string $method, ...$args): void 217 | { 218 | if ($this->sseMethodInProcess && $this->sseMethodInProcess !== $method) { 219 | $message = 'The SSE method `' . $method . '` cannot be called when `' . $this->sseMethodInProcess . '` is already in process.'; 220 | if (in_array($method, ['mergeSignals', 'removeSignals'])) { 221 | $message .= ' Ensure that you are not setting or removing signals inside `{% fragment %}` or `{% executescript %}` tags.'; 222 | } 223 | 224 | $this->throwException($message); 225 | } 226 | 227 | // Clean and end all existing output buffers. 228 | while (ob_get_level() > 0) { 229 | ob_end_clean(); 230 | } 231 | 232 | $this->getSseGenerator()->$method(...$args); 233 | 234 | $this->sseMethodInProcess = null; 235 | 236 | // Start a new output buffer to capture any subsequent inline content. 237 | ob_start(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 |