turbo_boost-commands 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10818d814e82b9fa6e91ad3416e8bad5ea6b4c45e48c1bb8853a0ebdde5c2ce2
4
- data.tar.gz: 309187d2ade4580b683116ab2ca2429f9e7de392cfe6b8522065f5f7c3857409
3
+ metadata.gz: 7e92a035ce993da7f6d137fd41110829a28ee782537dde9ec75bcec627793517
4
+ data.tar.gz: d233d3d7c5c5e238c9605284e42d9d4705d4485c3c947ef682788611a66e8642
5
5
  SHA512:
6
- metadata.gz: e0cf89d7064ccfa4934390bf44644aac52dd86d6faf6f1800e24d33cd2cfa94960c5f86c0aa333095fc708b9b5033d59aecb2ca6320daf48be177c3ba7ea5b6c
7
- data.tar.gz: a9362b9d2d5c72145d8484c586274bbbb3cb1f54e6c8b6802038f0f7bd2db20355aa33d0cec2cbbfaf8b1174286c292868efeeb516134ae20cc0665bd0aeb946
6
+ metadata.gz: d367bad2808120c960b006f87e17b632541fb427cb599e54734b2d50da6c2556623d82f2542fcfdf2ff614c5a7769842a66f1b935d762614a7988236cabdb04c
7
+ data.tar.gz: 522d2b1005e4a8bb37480b3a9bec79f81d2caa6c80423235449bad2abde0479dc4bf45ea2ff2db21613c57cc0ba1312554c3c33fc4f03afa0aadc3bf7d39e628
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </h1>
10
10
  <p align="center">
11
11
  <a href="http://blog.codinghorror.com/the-best-code-is-no-code-at-all/">
12
- <img alt="Lines of Code" src="https://img.shields.io/badge/loc-1736-47d299.svg" />
12
+ <img alt="Lines of Code" src="https://img.shields.io/badge/loc-1783-47d299.svg" />
13
13
  </a>
14
14
  <a href="https://codeclimate.com/github/hopsoft/turbo_boost-commands/maintainability">
15
15
  <img src="https://api.codeclimate.com/v1/badges/fe1162a742fe83a4fdfd/maintainability" />
@@ -62,6 +62,7 @@
62
62
 
63
63
  - [Why TurboBoost Commands?](#why-turboboost-commands)
64
64
  - [Sponsors](#sponsors)
65
+ - [Open Source projects like TurboBoost rely on your support](#open-source-projects-like-turboboost-rely-on-your-support)
65
66
  - [Dependencies](#dependencies)
66
67
  - [Setup](#setup)
67
68
  - [Configuration](#configuration)
@@ -75,7 +76,12 @@
75
76
  - [Setting Instance Variables](#setting-instance-variables)
76
77
  - [Prevent Controller Action](#prevent-controller-action)
77
78
  - [Broadcasting Turbo Streams](#broadcasting-turbo-streams)
78
- - [Tracking Page State](#tracking-page-state)
79
+ - [State](#state)
80
+ - [Server-State](#server-state)
81
+ - [Now-State](#now-state)
82
+ - [Client-State](#client-state)
83
+ - [Page-State](#page-state)
84
+ - [State Resolution](#state-resolution)
79
85
  - [Community](#community)
80
86
  - [Developing](#developing)
81
87
  - [Notable Files](#notable-files)
@@ -113,8 +119,6 @@ Namely,
113
119
  3. **(Re)render to reflect the new state**
114
120
  4. _repeat..._
115
121
 
116
- _The primary distinction being that **state is wholly managed by the server**._
117
-
118
122
  Commands are executed via a Rails `before_action` which means that reactivity runs over HTTP.
119
123
  _**Web sockets are NOT used for the reactive critical path!** 🎉_
120
124
  This also means that standard Rails mechanics drive their behavior.
@@ -134,10 +138,10 @@ Your contribution will help drive the evolution of **TurboBoost**, enabling new
134
138
 
135
139
  <p>
136
140
  <a href="https://donate.stripe.com/fZe9EjfhZbZRdeE9AA?utm_source=github&utm_medium=readme&utm_campaign=hopsoft&utm_content=turbo_boost-commands">
137
- <img src="https://img.shields.io/badge/Donate_with_Stripe-635bff?style=flat&labelColor=c4b8ff&logo=Stripe&logoColor=635bff&logoSize=auto" alt="Make a one-time Stripe donation" height="32" />
141
+ <img src="https://img.shields.io/badge/Donate_with_Stripe-635bff?style=flat&labelColor=c4b8ff&logo=Stripe&logoColor=635bff&logoSize=auto" alt="Make a one-time Stripe donation" height="24" />
138
142
  </a>
139
143
  <a href="https://commerce.coinbase.com/checkout/0a6079bf-5c7a-4a93-a943-401bba8981a0?utm_source=github&utm_medium=readme&utm_campaign=hopsoft&utm_content=turbo_boost-commands">
140
- <img src="https://img.shields.io/badge/Donate_with_Coinbase-0052ff.svg?style=flat&logoSize=30&labelColor=a3c4ff&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3Qgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIgZmlsbD0iIzAwNTJmZiIgcng9IjUxMiIvPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik01MTIuMTQ3IDY5MmMtOTkuNDUgMC0xODAtODAuNTUtMTgwLTE4MHM4MC41NS0xODAgMTgwLTE4MGM4OS4xIDAgMTYzLjA1IDY0Ljk1IDE3Ny4zIDE1MGgxODEuMzVjLTE1LjMtMTg0LjgtMTcwLTMzMC0zNTguNjUtMzMwLTE5OC43NSAwLTM2MCAxNjEuMjUtMzYwIDM2MHMxNjEuMjUgMzYwIDM2MCAzNjBjMTg4LjY1IDAgMzQzLjM1LTE0NS4yIDM1OC42NS0zMzBoLTE4MS41Yy0xNC4yNSA4NS4wNS04OC4yNSAxNTAtMTc3LjE1IDE1MHoiLz48L3N2Zz4=" alt="Make a one-time Coinbase donation" height="32" />
144
+ <img src="https://img.shields.io/badge/Donate_with_Coinbase-0052ff.svg?style=flat&logoSize=30&labelColor=a3c4ff&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHJlY3Qgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIgZmlsbD0iIzAwNTJmZiIgcng9IjUxMiIvPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik01MTIuMTQ3IDY5MmMtOTkuNDUgMC0xODAtODAuNTUtMTgwLTE4MHM4MC41NS0xODAgMTgwLTE4MGM4OS4xIDAgMTYzLjA1IDY0Ljk1IDE3Ny4zIDE1MGgxODEuMzVjLTE1LjMtMTg0LjgtMTcwLTMzMC0zNTguNjUtMzMwLTE5OC43NSAwLTM2MCAxNjEuMjUtMzYwIDM2MHMxNjEuMjUgMzYwIDM2MCAzNjBjMTg4LjY1IDAgMzQzLjM1LTE0NS4yIDM1OC42NS0zMzBoLTE4MS41Yy0xNC4yNSA4NS4wNS04OC4yNSAxNTAtMTc3LjE1IDE1MHoiLz48L3N2Zz4=" alt="Make a one-time Coinbase donation" height="24" />
141
145
  </a>
142
146
  </p>
143
147
 
@@ -206,7 +210,7 @@ TurboBoost::Commands.config.tap do |config|
206
210
  # opt-[in/out] of precompiling TurboBoost assets (*true, false)
207
211
  config.precompile_assets = true
208
212
 
209
- # opt-[in/out] of forgery protection (true, *false)
213
+ # opt-[in/out] of forgery protection (*true, false)
210
214
  config.protect_from_forgery = true
211
215
 
212
216
  # opt-[in/out] of raising an error when an invalid command is invoked (true, false, *"development", "test", "production")
@@ -214,6 +218,9 @@ TurboBoost::Commands.config.tap do |config|
214
218
 
215
219
  # opt-[in/out] of state resolution (true, *false)
216
220
  config.resolve_state = true
221
+
222
+ # opt-[in/out] of verifying the client browser (*true, false)
223
+ config.verify_client = true
217
224
  end
218
225
  ```
219
226
 
@@ -527,41 +534,157 @@ _Learn more about Turbo Stream broadcasting by reading through the
527
534
  > [!NOTE]
528
535
  > `broadcast_invoke_later_to` is a [TurboBoost Streams](https://github.com/hopsoft/turbo_boost-streams#broadcasting) feature.
529
536
 
530
- ### Tracking Page State
537
+ ## State
538
+
539
+ TurboBoost manages various forms of state to provide a terrific reactive user experience.
540
+
541
+ Here’s a breakdown of each type:
542
+
543
+ ### Server-State
544
+
545
+ Server-State is the persistent state that the server used for the most recent render.
546
+ This state is signed, ensuring data integrity and security.
547
+
548
+ The client includes this signed state along with its own optimistic changes whenever a Command is invoked.
549
+ The server can then compute the difference between the Client-State and the Server-State,
550
+ allowing you to accept or reject the client's optimistic changes.
551
+
552
+ This ensures the server remains the single source of truth.
553
+
554
+ Server-State can be accessed within Commands like so.
555
+
556
+ ```ruby
557
+ state[:key] = "value"
558
+ state[:key]
559
+ #=> "value"
560
+ ```
561
+
562
+ Server-State is also accessible in controllers and views.
563
+
564
+ ```ruby
565
+ # controller
566
+ turbo_boost.state[:key] = "value"
567
+ turbo_boost.state[:key]
568
+ #=> "value"
569
+ ```
570
+
571
+ ```erb
572
+ <%
573
+ # view
574
+ turbo_boost.state[:key] = "value"
575
+ turbo_boost.state[:key]
576
+ #=> "value"
577
+ %>
578
+ ```
579
+
580
+ ### Now-State
581
+
582
+ Now-State is ephemeral server side state that only exists for the current render cycle.
583
+ Similar to `flash.now` in Rails, this state is discarded after rendering.
584
+
585
+ It’s useful for managing temporary data that doesn’t need to persist beyond the current request.
586
+
587
+ Now-State can be accessed within Commands like so.
588
+
589
+ ```ruby
590
+ state.now[:key] = "value"
591
+ state.now[:key]
592
+ #=> "value"
593
+ ```
594
+
595
+ Now-State is also accessible in controllers and views.
596
+
597
+ ```ruby
598
+ # controller
599
+ turbo_boost.state.now[:key] = "value"
600
+ turbo_boost.state.now[:key]
601
+ #=> "value"
602
+ ```
603
+
604
+ ```erb
605
+ <%
606
+ # view
607
+ turbo_boost.state.now[:key] = "value"
608
+ turbo_boost.state.now[:key]
609
+ #=> "value"
610
+ %>
611
+ ```
612
+
613
+ ### Client-State
614
+
615
+ Client-State is a mutable version of the signed Server-State, wrapped in an observable JavaScript proxy.
616
+ This allows for sophisticated techniques like data binding via custom JavaScript, Stimulus controllers, or web components.
617
+
618
+ Client-State enables immediate UI updates, providing a fast and smooth user experience while the server
619
+ [resolves state](#state-resolution) differences whenever Commands are invoked.
620
+
621
+ Client-State can be accessed on the client like so.
622
+
623
+ ```js
624
+ TurboBoost.State.current['key'] = 'value'
625
+ TurboBoost.State.current['key']
626
+ //=> 'value'
627
+ ```
628
+
629
+ ### Page-State
630
+
631
+ Page-State is managed by the client and used to remember element attribute values between server renders.
632
+ It’s best for tracking transient user interactions, such as - which elements are visible, open/closed, their position, etc.
531
633
 
532
- You can opt-in to remember transient page state when using Rails tag helpers with `turbo_boost[:remember]` to track
533
- element attribute values between requests.
634
+ This enhances the user experience by maintaining the state of UI elements between renders.
635
+ When invoking commands, the client sends the Page-State to the server, allowing it to preserve element attributes when rendering.
636
+ _The client also checks and restores Page-State whenever the DOM changes if needed._
637
+
638
+ You can opt-in to remember Page-State with Rails tag helpers via the `turbo_boost[:remember]` option.
534
639
 
535
640
  ```erb
536
641
  <%= tag.details id: "page-state-example", open: "open", turbo_boost: { remember: [:open] } do %>
537
- <summary>Page State Example</summary>
642
+ <summary>Page-State Example</summary>
538
643
  Content...
539
644
  <% end %>
540
645
  ```
541
646
 
542
- The code above will be expanded to this HTML.
647
+ This will remember whether the `details` element is open or closed.
543
648
 
544
- ```html
545
- <details id="page-state-example" open="open" data-turbo-boost-state-attributes="['open']">
546
- <summary>Page State Example</summary>
547
- Content...
548
- </details>
549
- ```
649
+ __That's it!__ You're done.
650
+
651
+ > [!NOTE]
652
+ > Page-State tracking works with all element attributes, including `aria`, `data`, and even custom attributes.
653
+ > Elements must have a unique `id` to participate in Page-State tracking.
550
654
 
551
- If the user closes the details element and invokes a command or performs a request,
552
- the server will pre-render the markup with the current page state preserving the `open` attribute value.
553
- _The client also ensures that remembered attributes are restored after DOM mutations._
655
+ ### State Resolution
554
656
 
555
- Several things happen when you use `turbo_boost[:remember]` to track page state.
657
+ Commands can perform state resolution by implementing the `resolve_state` method.
556
658
 
557
- 1. The client builds the current page state before emitting requests to the server.
558
- 1. The server uses the page state when rendering the response.
559
- 1. The client client verifies the page state and restores attribute values _(if necessary)_ after the DOM updates.
659
+ The Command has access to all forms of state, so you should use explicit access during resolution.
560
660
 
561
- This feature works with all attributes, including aria, data, and custom attributes.
661
+ You can access both the signed Server-State and the optimistc Client-State from within the Command like so.
562
662
 
563
- > [!NOTE]
564
- > Elements must have a unique `id` assigned to participate in page state tracking.
663
+ ```ruby
664
+ class ExampleCommand < TurboBoost::Commands::Command
665
+
666
+ def resolve_state
667
+ state.signed #=> the Server-State (from the last render)
668
+ state.unsigned #=> the optimistic Client-State
669
+ # compare and resolve the delta
670
+ end
671
+ end
672
+ ```
673
+
674
+ > [!TIP]
675
+ > State resolution can involve data lookups, updates to persistent data stores, calls to 3rd party APIs, etc.
676
+
677
+ You can opt-in to state resolution with the following config option.
678
+
679
+ ```ruby
680
+ # config/initializers/turbo_boost.rb
681
+ TurboBoost::Commands.config.tap do |config|
682
+ config.resolve_state = true
683
+ end
684
+ ```
685
+
686
+ > [!TIP]
687
+ > TurboBoost State mechanics can also be used independent of Commands with standard Hotwire techniques.
565
688
 
566
689
  ## Community
567
690
 
@@ -1,2 +1,2 @@
1
- var mt=Object.defineProperty,lt=Object.defineProperties;var ft=Object.getOwnPropertyDescriptors;var X=Object.getOwnPropertySymbols;var pt=Object.prototype.hasOwnProperty,bt=Object.prototype.propertyIsEnumerable;var K=(t,e,r)=>e in t?mt(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,s=(t,e)=>{for(var r in e||(e={}))pt.call(e,r)&&K(t,r,e[r]);if(X)for(var r of X(e))bt.call(e,r)&&K(t,r,e[r]);return t},p=(t,e)=>lt(t,ft(e));var gt="TurboBoost-Command",y={boost:"text/vnd.turbo-boost.html",stream:"text/vnd.turbo-stream.html",html:"text/html",xhtml:"application/xhtml+xml",json:"application/json"},ht=(t={})=>{t=s({},t);let e=(t.Accept||"").split(",").map(r=>r.trim()).filter(r=>r.length);return e.unshift(y.boost,y.stream,y.html,y.xhtml),t.Accept=[...new Set(e)].join(", "),t["Content-Type"]=y.json,t["X-Requested-With"]="XMLHttpRequest",t},vt=t=>{if(t){let[e,r,o]=t.split(", ");return{name:e,status:r,strategy:o}}return{}},f={prepare:ht,tokenize:vt,RESPONSE_HEADER:gt};var yt=t=>{document.body.insertAdjacentHTML("beforeend",t)},Et=t=>{var h,k,v,C;let r=new DOMParser().parseFromString(t,"text/html"),o=document.querySelector("head"),n=document.querySelector("body"),i=r.querySelector("head"),c=r.querySelector("body");o&&i&&((k=(h=TurboBoost==null?void 0:TurboBoost.Streams)==null?void 0:h.morph)==null||k.method(o,i)),n&&c&&((C=(v=TurboBoost==null?void 0:TurboBoost.Streams)==null?void 0:v.morph)==null||C.method(n,c))},w=(t,e)=>{if(t&&e){if(t.match(/^Append$/i))return yt(e);if(t.match(/^Replace$/i))return Et(e)}};var B={};addEventListener("turbo:before-fetch-response",t=>{let e=t.target.closest("turbo-frame");e!=null&&e.id&&(e!=null&&e.src)&&(B[e.id]=e.src);let{fetchResponse:r}=t.detail,o=r.header(f.RESPONSE_HEADER);if(!o)return;t.preventDefault();let{strategy:n}=f.tokenize(o);r.responseHTML.then(i=>w(n,i))});addEventListener("turbo:frame-load",t=>{let e=t.target.closest("turbo-frame");e.dataset.src=B[e.id]||e.src||e.dataset.src,delete B[e.id]});var At={frameAttribute:"data-turbo-frame",methodAttribute:"data-turbo-method",commandAttribute:"data-turbo-command",confirmAttribute:"data-turbo-confirm",stateAttributesAttribute:"data-turbo-boost-state-attributes"},d=s({},At);var a={start:"turbo-boost:command:start",success:"turbo-boost:command:success",finish:"turbo-boost:command:finish",abort:"turbo-boost:command:abort",clientError:"turbo-boost:command:client-error",serverError:"turbo-boost:command:server-error"},E={stateChange:"turbo-boost:state:change",stateInitialize:"turbo-boost:state:initialize"};function u(t,e,r={}){return new Promise(o=>{r=r||{},r.detail=r.detail||{},e=e||document;let n=new CustomEvent(t,p(s({},r),{bubbles:!0}));e.dispatchEvent(n),o(n)})}var L={};function St(t){L[t.id]=t}function xt(t){delete L[t]}var O={add:St,remove:xt,get commands(){return[...Object.values(L)]},get length(){return Object.keys(L).length}};var G={method:t=>Promise.resolve(confirm(t))},kt=t=>t.detail.driver==="method",Ct=t=>{if(t.detail.driver!=="form")return!1;let e=t.target,r=e.closest("turbo-frame"),o=e.closest(`[${d.frameAttribute}]`);return!!(r||o)},wt=t=>kt(t)||Ct(t);document.addEventListener(a.start,async t=>{let e=t.target.getAttribute(d.confirmAttribute);if(!e||(t.detail.confirmation=!0,wt(t)))return;await G.method(e)||t.preventDefault()});var Q=G;var l=[],I;function Lt(t,e){let r=l.find(o=>o.name===t);return r&&l.splice(l.indexOf(r),1),l=[{name:t,selectors:e},...l],document.removeEventListener(t,I,!0),document.addEventListener(t,I,!0),s({},l.find(o=>o.name===t))}function Ot(t){return l.find(e=>e.selectors.find(r=>Array.from(document.querySelectorAll(r)).find(o=>o===t)))}function Tt(t,e){let r=Ot(e);return r&&r.name===t}var m={register:Lt,isRegisteredForElement:Tt,get events(){return[...l]},set handler(t){I=t}};function Rt(t){return t.closest(`[${d.commandAttribute}]`)}function Pt(t){return t.closest("turbo-frame[src]")||t.closest("turbo-frame[data-turbo-frame-src]")||t.closest("turbo-frame")}function $t(t,e={}){if(t.tagName.toLowerCase()!=="select")return e.value=t.value||null;if(!t.multiple)return e.value=t.options[t.selectedIndex].value;e.values=Array.from(t.options).reduce((r,o)=>(o.selected&&r.push(o.value),r),[])}function _t(t){let e=Array.from(t.attributes).reduce((r,o)=>{let n=o.value;return r[o.name]=n,r},{});return e.tag=t.tagName,e.checked=!!t.checked,e.disabled=!!t.disabled,$t(t,e),delete e.class,delete e.action,delete e.href,delete e[d.commandAttribute],delete e[d.frameAttribute],e}var A={buildAttributePayload:_t,findClosestCommand:Rt,findClosestFrameWithSource:Pt};var Dt=(t,e={})=>{let r=t.querySelector('input[name="turbo_boost_command"]')||document.createElement("input");r.type="hidden",r.name="turbo_boost_command",r.value=JSON.stringify(e),t.contains(r)||t.appendChild(r)},Y={invokeCommand:Dt};function Nt(t){setTimeout(()=>u(a.finish,t.target,{detail:t.detail}))}var Bt=[a.abort,a.serverError,a.success];Bt.forEach(t=>addEventListener(t,Nt));addEventListener(a.finish,t=>O.remove(t.detail.id),!0);var Z={events:a};var It=t=>{let e=document.createElement("a");return e.href=t,new URL(e)},tt={get commandInvocationURL(){return It("/turbo-boost-command-invocation")}};var et=t=>{let e=`Unexpected error performing a TurboBoost Command! ${t.message}`;u(Z.events.clientError,document,{detail:{message:e,error:t}},!0)},jt=t=>{let{strategy:e}=f.tokenize(t.headers.get(f.RESPONSE_HEADER));t.text().then(r=>w(e,r))},T=(t={})=>{try{fetch(tt.commandInvocationURL.href,{method:"POST",headers:f.prepare({}),body:JSON.stringify(t)}).then(jt).catch(et)}catch(e){et(e)}};var qt=(t,e)=>T(e),j={invokeCommand:qt};var S,q,zt=()=>{S=null,q=null},Ft=(t,e={})=>{S=t,q=e},Ht=t=>{try{if(!S||t.getAttribute("method")!==S.dataset.turboMethod||t.getAttribute("action")!==S.href)return;let e=t.querySelector('input[name="turbo_boost_command"]')||document.createElement("input");e.type="hidden",e.name="turbo_boost_command",e.value=JSON.stringify(q),t.contains(e)||t.appendChild(e)}finally{zt()}};document.addEventListener("submit",t=>Ht(t.target),!0);var rt={invokeCommand:Ft};var Jt=(t,e={})=>T(e),ot={invokeCommand:Jt};function z(t,e){return e=e||{dataset:{}},t.href||e.src||e.dataset.src||location.href}function Mt(t){let e=A.findClosestFrameWithSource(t),{turboFrame:r,turboMethod:o}=t.dataset;return t.tagName.toLowerCase()==="form"?{name:"form",reason:"Element is a form.",frame:e,src:t.action,invokeCommand:Y.invokeCommand}:o!=null&&o.length?{name:"method",reason:"Element defines data-turbo-method.",frame:e,src:t.href,invokeCommand:rt.invokeCommand}:r&&r!=="_self"?(e=document.getElementById(r),{name:"frame",reason:"element targets a frame that is not _self",frame:e,src:z(t,e),invokeCommand:j.invokeCommand}):(!r||r==="_self")&&e?{name:"frame",reason:"element does NOT target a frame or targets _self and is contained by a frame",frame:e,src:z(t,e),invokeCommand:j.invokeCommand}:{name:"window",reason:"element matches one or more of the following conditions (targets _top, does NOT target a frame, is NOT contained by a frame)",frame:null,src:z(t),invokeCommand:ot.invokeCommand}}var F={find:Mt};var P="unknown",nt=!1,R=[],b={debug:Object.values(a),info:Object.values(a),warn:[a.abort,a.clientError,a.serverError],error:[a.clientError,a.serverError],unknown:[]},Ut=t=>{if(!b[P].includes(t.type)||typeof console[P]!="function")return!1;let{detail:e}=t;if(!e.id)return!0;let r=`${t.type}-${e.id}`;return R.includes(r)?!1:(R.length>16&&R.shift(),R.push(r),!0)},Vt=t=>b.error.includes(t.type)?"error":b.warn.includes(t.type)?"warn":b.info.includes(t.type)?"info":b.debug.includes(t.type)?"debug":"log",Wt=t=>{if(Ut(t)){let{target:e,type:r,detail:o}=t,n=o.id||"",i=o.name||"",c="";o.startedAt&&(c=`${Date.now()-o.startedAt}ms `);let h=r.split(":"),k=h.pop(),v=`%c${h.join(":")}:%c${k}`,C=[`%c${i}`,`%c${c}`,v];console[Vt(t)](C.join(" ").replace(/\s{2,}/g," "),"color:deepskyblue","color:lime","color:darkgray",v.match(/abort|error/i)?"color:red":"color:deepskyblue",{id:n,detail:o,target:e})}};nt||(nt=!0,Object.values(a).forEach(t=>addEventListener(t,e=>Wt(e))));var at={get level(){return P},set level(t){return Object.keys(b).includes(t)||(t="unknown"),P=t}};var H;function $(t,e=null){if(!t||typeof t!="object")return t;let r=new Proxy(t,{deleteProperty(o,n){return delete o[n],u(E.stateChange,document,{detail:{state:H}}),!0},set(o,n,i,c){return o[n]=$(i,this),u(E.stateChange,document,{detail:{state:H}}),!0}});if(Array.isArray(t))t.forEach((o,n)=>t[n]=$(o,r));else if(typeof t=="object")for(let[o,n]of Object.entries(t))t[o]=$(n,r);return e||(H=r),r}var J=$;var st=(t,e,r,o=1)=>{if(o>20)return;let n=document.getElementById(t);if(n!=null&&n.isConnected)return n.setAttribute(e,r);setTimeout(()=>st(t,e,r,o+1),o*5)},Xt=()=>Array.from(document.querySelectorAll(`[id][${d.stateAttributesAttribute}]`)).reduce((e,r)=>{let o=JSON.parse(r.getAttribute(d.stateAttributesAttribute));if(r.id){let n=o.reduce((i,c)=>(r.hasAttribute(c)&&(i[c]=r.getAttribute(c)||c),i),{});Object.values(n).length&&(e[r.id]=n)}return e},{}),Kt=(t={})=>{for(let[e,r]of Object.entries(t))for(let[o,n]of Object.entries(r))st(e,o,n)},_={buildState:Xt,restoreState:Kt};function Gt(t,e){return typeof e!="object"&&(e={}),localStorage.setItem(String(t),JSON.stringify(e))}function Qt(t){let e=localStorage.getItem(String(t));return e?JSON.parse(e):{}}var D={save:Gt,find:Qt};var M="TurboBoost::State",U={pages:{},signed:null,unsigned:{}},N=null,x={},V=()=>{let t=s(s({},U),D.find(M));N=t.signed,x=J(t.unsigned),t.pages[location.pathname]=t.pages[location.pathname]||{},_.restoreState(t.pages[location.pathname])},W=()=>{let t=s(s({},U),D.find(M)),e={signed:N||t.signed,unsigned:s(s({},t.unsigned),x),pages:s({},t.pages)},r=location.pathname,o=_.buildState();Object.values(o).length?e.pages[r]=o:delete e.pages[r],D.save(M,e)},Yt=t=>{let e=s(s({},U),JSON.parse(t));N=e.signed,x=J(e.unsigned),W(),u(E.stateInitialize,document,{detail:x})};addEventListener("DOMContentLoaded",V);addEventListener("turbo:morph",V);addEventListener("turbo:render",V);addEventListener("turbo:before-fetch-request",W);addEventListener("beforeunload",W);var g={initialize:Yt,buildPageState:_.buildState,get signed(){return N},get unsigned(){return x}};function Zt(){return("10000000-1000-4000-8000"+-1e11).replace(/[018]/g,t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16))}var it={v4:Zt};var dt="0.3.1";var te=self.TurboBoost||{},ut={VERSION:dt,active:!1,confirmation:Q,logger:at,schema:d,events:a,registerEventDelegate:m.register,get eventDelegates(){return m.events}};function ct(t,e){var r;return{csrfToken:(r=document.querySelector('meta[name="csrf-token"]'))==null?void 0:r.getAttribute("content"),id:t,name:e.getAttribute(d.commandAttribute),elementId:e.id.length?e.id:null,elementAttributes:A.buildAttributePayload(e),startedAt:Date.now(),state:{page:g.buildPageState(),signed:g.signed,unsigned:g.unsigned}}}async function ee(t){let e,r={};try{if(e=A.findClosestCommand(t.target),!e||!m.isRegisteredForElement(t.type,e))return;let o=it.v4(),n=F.find(e),i=p(s({},ct(o,e)),{driver:n.name,frameId:n.frame?n.frame.id:null,src:n.src}),c=await u(a.start,e,{cancelable:!0,detail:i});if(c.defaultPrevented||c.detail.confirmation&&t.defaultPrevented)return u(a.abort,e,{detail:{message:`An event handler for '${a.start}' prevented default behavior and blocked command invocation!`,source:c}});switch(n=F.find(e),i=p(s({},ct(o,e)),{driver:n.name,frameId:n.frame?n.frame.id:null,src:n.src}),O.add(i),["frame","window"].includes(n.name)&&t.preventDefault(),n.name){case"method":return n.invokeCommand(e,i);case"form":return n.invokeCommand(e,i,t);case"frame":return n.invokeCommand(n.frame,i);case"window":return n.invokeCommand(self,i)}}catch(o){u(a.clientError,e,{detail:p(s({},r),{error:o})})}}self.TurboBoost=s({},te);self.TurboBoost.Commands||(m.handler=ee,m.register("click",[`[${d.commandAttribute}]`]),m.register("submit",[`form[${d.commandAttribute}]`]),m.register("toggle",[`details[${d.commandAttribute}]`]),m.register("change",[`input[${d.commandAttribute}]`,`select[${d.commandAttribute}]`,`textarea[${d.commandAttribute}]`]),self.TurboBoost.Commands=ut,self.TurboBoost.State={initialize:g.initialize,get current(){return g.unsigned}});var gr=ut;export{gr as default};
1
+ var mt=Object.defineProperty,lt=Object.defineProperties;var ft=Object.getOwnPropertyDescriptors;var X=Object.getOwnPropertySymbols;var pt=Object.prototype.hasOwnProperty,bt=Object.prototype.propertyIsEnumerable;var K=(t,e,r)=>e in t?mt(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r,s=(t,e)=>{for(var r in e||(e={}))pt.call(e,r)&&K(t,r,e[r]);if(X)for(var r of X(e))bt.call(e,r)&&K(t,r,e[r]);return t},p=(t,e)=>lt(t,ft(e));var gt="TurboBoost-Command",y={boost:"text/vnd.turbo-boost.html",stream:"text/vnd.turbo-stream.html",html:"text/html",xhtml:"application/xhtml+xml",json:"application/json"},ht=(t={})=>{t=s({},t);let e=(t.Accept||"").split(",").map(r=>r.trim()).filter(r=>r.length);return e.unshift(y.boost,y.stream,y.html,y.xhtml),t.Accept=[...new Set(e)].join(", "),t["Content-Type"]=y.json,t["X-Requested-With"]="XMLHttpRequest",t},vt=t=>{if(t){let[e,r,o]=t.split(", ");return{name:e,status:r,strategy:o}}return{}},f={prepare:ht,tokenize:vt,RESPONSE_HEADER:gt};var yt=t=>{document.body.insertAdjacentHTML("beforeend",t)},Et=t=>{var h,k,v,C;let r=new DOMParser().parseFromString(t,"text/html"),o=document.querySelector("head"),n=document.querySelector("body"),i=r.querySelector("head"),c=r.querySelector("body");o&&i&&((k=(h=TurboBoost==null?void 0:TurboBoost.Streams)==null?void 0:h.morph)==null||k.method(o,i)),n&&c&&((C=(v=TurboBoost==null?void 0:TurboBoost.Streams)==null?void 0:v.morph)==null||C.method(n,c))},w=(t,e)=>{if(t&&e){if(t.match(/^Append$/i))return yt(e);if(t.match(/^Replace$/i))return Et(e)}};var B={};addEventListener("turbo:before-fetch-response",t=>{let e=t.target.closest("turbo-frame");e!=null&&e.id&&(e!=null&&e.src)&&(B[e.id]=e.src);let{fetchResponse:r}=t.detail,o=r.header(f.RESPONSE_HEADER);if(!o)return;t.preventDefault();let{strategy:n}=f.tokenize(o);r.responseHTML.then(i=>w(n,i))});addEventListener("turbo:frame-load",t=>{let e=t.target.closest("turbo-frame");e.dataset.src=B[e.id]||e.src||e.dataset.src,delete B[e.id]});var At={frameAttribute:"data-turbo-frame",methodAttribute:"data-turbo-method",commandAttribute:"data-turbo-command",confirmAttribute:"data-turbo-confirm",stateAttributesAttribute:"data-turbo-boost-state-attributes"},d=s({},At);var a={start:"turbo-boost:command:start",success:"turbo-boost:command:success",finish:"turbo-boost:command:finish",abort:"turbo-boost:command:abort",clientError:"turbo-boost:command:client-error",serverError:"turbo-boost:command:server-error"},E={stateChange:"turbo-boost:state:change",stateInitialize:"turbo-boost:state:initialize"};function u(t,e,r={}){return new Promise(o=>{r=r||{},r.detail=r.detail||{},e=e||document;let n=new CustomEvent(t,p(s({},r),{bubbles:!0}));e.dispatchEvent(n),o(n)})}var L={};function St(t){L[t.id]=t}function xt(t){delete L[t]}var O={add:St,remove:xt,get commands(){return[...Object.values(L)]},get length(){return Object.keys(L).length}};var G={method:t=>Promise.resolve(confirm(t))},kt=t=>t.detail.driver==="method",Ct=t=>{if(t.detail.driver!=="form")return!1;let e=t.target,r=e.closest("turbo-frame"),o=e.closest(`[${d.frameAttribute}]`);return!!(r||o)},wt=t=>kt(t)||Ct(t);document.addEventListener(a.start,async t=>{let e=t.target.getAttribute(d.confirmAttribute);if(!e||(t.detail.confirmation=!0,wt(t)))return;await G.method(e)||t.preventDefault()});var Q=G;var l=[],I;function Lt(t,e){let r=l.find(o=>o.name===t);return r&&l.splice(l.indexOf(r),1),l=[{name:t,selectors:e},...l],document.removeEventListener(t,I,!0),document.addEventListener(t,I,!0),s({},l.find(o=>o.name===t))}function Ot(t){return l.find(e=>e.selectors.find(r=>Array.from(document.querySelectorAll(r)).find(o=>o===t)))}function Tt(t,e){let r=Ot(e);return r&&r.name===t}var m={register:Lt,isRegisteredForElement:Tt,get events(){return[...l]},set handler(t){I=t}};function Rt(t){return t.closest(`[${d.commandAttribute}]`)}function Pt(t){return t.closest("turbo-frame[src]")||t.closest("turbo-frame[data-turbo-frame-src]")||t.closest("turbo-frame")}function $t(t,e={}){if(t.tagName.toLowerCase()!=="select")return e.value=t.value||null;if(!t.multiple)return e.value=t.options[t.selectedIndex].value;e.values=Array.from(t.options).reduce((r,o)=>(o.selected&&r.push(o.value),r),[])}function _t(t){let e=Array.from(t.attributes).reduce((r,o)=>{let n=o.value;return r[o.name]=n,r},{});return e.tag=t.tagName,e.checked=!!t.checked,e.disabled=!!t.disabled,$t(t,e),delete e.class,delete e.action,delete e.href,delete e[d.commandAttribute],delete e[d.frameAttribute],e}var A={buildAttributePayload:_t,findClosestCommand:Rt,findClosestFrameWithSource:Pt};var Dt=(t,e={})=>{let r=t.querySelector('input[name="turbo_boost_command"]')||document.createElement("input");r.type="hidden",r.name="turbo_boost_command",r.value=JSON.stringify(e),t.contains(r)||t.appendChild(r)},Y={invokeCommand:Dt};function Nt(t){setTimeout(()=>u(a.finish,t.target,{detail:t.detail}))}var Bt=[a.abort,a.serverError,a.success];Bt.forEach(t=>addEventListener(t,Nt));addEventListener(a.finish,t=>O.remove(t.detail.id),!0);var Z={events:a};var It=t=>{let e=document.createElement("a");return e.href=t,new URL(e)},tt={get commandInvocationURL(){return It("/turbo-boost-command-invocation")}};var et=t=>{let e=`Unexpected error performing a TurboBoost Command! ${t.message}`;u(Z.events.clientError,document,{detail:{message:e,error:t}},!0)},jt=t=>{let{strategy:e}=f.tokenize(t.headers.get(f.RESPONSE_HEADER));t.text().then(r=>w(e,r))},T=(t={})=>{try{fetch(tt.commandInvocationURL.href,{method:"POST",headers:f.prepare({}),body:JSON.stringify(t)}).then(jt).catch(et)}catch(e){et(e)}};var qt=(t,e)=>T(e),j={invokeCommand:qt};var S,q,zt=()=>{S=null,q=null},Ft=(t,e={})=>{S=t,q=e},Ht=t=>{try{if(!S||t.getAttribute("method")!==S.dataset.turboMethod||t.getAttribute("action")!==S.href)return;let e=t.querySelector('input[name="turbo_boost_command"]')||document.createElement("input");e.type="hidden",e.name="turbo_boost_command",e.value=JSON.stringify(q),t.contains(e)||t.appendChild(e)}finally{zt()}};document.addEventListener("submit",t=>Ht(t.target),!0);var rt={invokeCommand:Ft};var Jt=(t,e={})=>T(e),ot={invokeCommand:Jt};function z(t,e){return e=e||{dataset:{}},t.href||e.src||e.dataset.src||location.href}function Mt(t){let e=A.findClosestFrameWithSource(t),{turboFrame:r,turboMethod:o}=t.dataset;return t.tagName.toLowerCase()==="form"?{name:"form",reason:"Element is a form.",frame:e,src:t.action,invokeCommand:Y.invokeCommand}:o!=null&&o.length?{name:"method",reason:"Element defines data-turbo-method.",frame:e,src:t.href,invokeCommand:rt.invokeCommand}:r&&r!=="_self"?(e=document.getElementById(r),{name:"frame",reason:"element targets a frame that is not _self",frame:e,src:z(t,e),invokeCommand:j.invokeCommand}):(!r||r==="_self")&&e?{name:"frame",reason:"element does NOT target a frame or targets _self and is contained by a frame",frame:e,src:z(t,e),invokeCommand:j.invokeCommand}:{name:"window",reason:"element matches one or more of the following conditions (targets _top, does NOT target a frame, is NOT contained by a frame)",frame:null,src:z(t),invokeCommand:ot.invokeCommand}}var F={find:Mt};var P="unknown",nt=!1,R=[],b={debug:Object.values(a),info:Object.values(a),warn:[a.abort,a.clientError,a.serverError],error:[a.clientError,a.serverError],unknown:[]},Ut=t=>{if(!b[P].includes(t.type)||typeof console[P]!="function")return!1;let{detail:e}=t;if(!e.id)return!0;let r=`${t.type}-${e.id}`;return R.includes(r)?!1:(R.length>16&&R.shift(),R.push(r),!0)},Vt=t=>b.error.includes(t.type)?"error":b.warn.includes(t.type)?"warn":b.info.includes(t.type)?"info":b.debug.includes(t.type)?"debug":"log",Wt=t=>{if(Ut(t)){let{target:e,type:r,detail:o}=t,n=o.id||"",i=o.name||"",c="";o.startedAt&&(c=`${Date.now()-o.startedAt}ms `);let h=r.split(":"),k=h.pop(),v=`%c${h.join(":")}:%c${k}`,C=[`%c${i}`,`%c${c}`,v];console[Vt(t)](C.join(" ").replace(/\s{2,}/g," "),"color:deepskyblue","color:lime","color:darkgray",v.match(/abort|error/i)?"color:red":"color:deepskyblue",{id:n,detail:o,target:e})}};nt||(nt=!0,Object.values(a).forEach(t=>addEventListener(t,e=>Wt(e))));var at={get level(){return P},set level(t){return Object.keys(b).includes(t)||(t="unknown"),P=t}};var H;function $(t,e=null){if(!t||typeof t!="object")return t;let r=new Proxy(t,{deleteProperty(o,n){return delete o[n],u(E.stateChange,document,{detail:{state:H}}),!0},set(o,n,i,c){return o[n]=$(i,this),u(E.stateChange,document,{detail:{state:H}}),!0}});if(Array.isArray(t))t.forEach((o,n)=>t[n]=$(o,r));else if(typeof t=="object")for(let[o,n]of Object.entries(t))t[o]=$(n,r);return e||(H=r),r}var J=$;var st=(t,e,r,o=1)=>{if(o>20)return;let n=document.getElementById(t);if(n!=null&&n.isConnected)return n.setAttribute(e,r);setTimeout(()=>st(t,e,r,o+1),o*5)},Xt=()=>Array.from(document.querySelectorAll(`[id][${d.stateAttributesAttribute}]`)).reduce((e,r)=>{let o=JSON.parse(r.getAttribute(d.stateAttributesAttribute));if(r.id){let n=o.reduce((i,c)=>(r.hasAttribute(c)&&(i[c]=r.getAttribute(c)||c),i),{});Object.values(n).length&&(e[r.id]=n)}return e},{}),Kt=(t={})=>{for(let[e,r]of Object.entries(t))for(let[o,n]of Object.entries(r))st(e,o,n)},_={buildState:Xt,restoreState:Kt};function Gt(t,e){return typeof e!="object"&&(e={}),localStorage.setItem(String(t),JSON.stringify(e))}function Qt(t){let e=localStorage.getItem(String(t));return e?JSON.parse(e):{}}var D={save:Gt,find:Qt};var M="TurboBoost::State",U={pages:{},signed:null,unsigned:{}},N=null,x={},V=()=>{let t=s(s({},U),D.find(M));N=t.signed,x=J(t.unsigned),t.pages[location.pathname]=t.pages[location.pathname]||{},_.restoreState(t.pages[location.pathname])},W=()=>{let t=s(s({},U),D.find(M)),e={signed:N||t.signed,unsigned:s(s({},t.unsigned),x),pages:s({},t.pages)},r=location.pathname,o=_.buildState();Object.values(o).length?e.pages[r]=o:delete e.pages[r],D.save(M,e)},Yt=t=>{let e=s(s({},U),JSON.parse(t));N=e.signed,x=J(e.unsigned),W(),u(E.stateInitialize,document,{detail:x})};addEventListener("DOMContentLoaded",V);addEventListener("turbo:morph",V);addEventListener("turbo:render",V);addEventListener("turbo:before-fetch-request",W);addEventListener("beforeunload",W);var g={initialize:Yt,buildPageState:_.buildState,get signed(){return N},get unsigned(){return x}};function Zt(){return("10000000-1000-4000-8000"+-1e11).replace(/[018]/g,t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16))}var it={v4:Zt};var dt="0.3.2";var te=self.TurboBoost||{},ut={VERSION:dt,active:!1,confirmation:Q,logger:at,schema:d,events:a,registerEventDelegate:m.register,get eventDelegates(){return m.events}};function ct(t,e){var r;return{csrfToken:(r=document.querySelector('meta[name="csrf-token"]'))==null?void 0:r.getAttribute("content"),id:t,name:e.getAttribute(d.commandAttribute),elementId:e.id.length?e.id:null,elementAttributes:A.buildAttributePayload(e),startedAt:Date.now(),state:{page:g.buildPageState(),signed:g.signed,unsigned:g.unsigned}}}async function ee(t){let e,r={};try{if(e=A.findClosestCommand(t.target),!e||!m.isRegisteredForElement(t.type,e))return;let o=it.v4(),n=F.find(e),i=p(s({},ct(o,e)),{driver:n.name,frameId:n.frame?n.frame.id:null,src:n.src}),c=await u(a.start,e,{cancelable:!0,detail:i});if(c.defaultPrevented||c.detail.confirmation&&t.defaultPrevented)return u(a.abort,e,{detail:{message:`An event handler for '${a.start}' prevented default behavior and blocked command invocation!`,source:c}});switch(n=F.find(e),i=p(s({},ct(o,e)),{driver:n.name,frameId:n.frame?n.frame.id:null,src:n.src}),O.add(i),["frame","window"].includes(n.name)&&t.preventDefault(),n.name){case"method":return n.invokeCommand(e,i);case"form":return n.invokeCommand(e,i,t);case"frame":return n.invokeCommand(n.frame,i);case"window":return n.invokeCommand(self,i)}}catch(o){u(a.clientError,e,{detail:p(s({},r),{error:o})})}}self.TurboBoost=s({},te);self.TurboBoost.Commands||(m.handler=ee,m.register("click",[`[${d.commandAttribute}]`]),m.register("submit",[`form[${d.commandAttribute}]`]),m.register("toggle",[`details[${d.commandAttribute}]`]),m.register("change",[`input[${d.commandAttribute}]`,`select[${d.commandAttribute}]`,`textarea[${d.commandAttribute}]`]),self.TurboBoost.Commands=ut,self.TurboBoost.State={initialize:g.initialize,get current(){return g.unsigned}});var gr=ut;export{gr as default};
2
2
  //# sourceMappingURL=commands.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../javascript/headers.js", "../../../javascript/renderer.js", "../../../javascript/turbo.js", "../../../javascript/schema.js", "../../../javascript/events.js", "../../../javascript/activity.js", "../../../javascript/confirmation.js", "../../../javascript/delegates.js", "../../../javascript/elements.js", "../../../javascript/drivers/form.js", "../../../javascript/lifecycle.js", "../../../javascript/urls.js", "../../../javascript/invoker.js", "../../../javascript/drivers/frame.js", "../../../javascript/drivers/method.js", "../../../javascript/drivers/window.js", "../../../javascript/drivers/index.js", "../../../javascript/logger.js", "../../../javascript/state/observable.js", "../../../javascript/state/page.js", "../../../javascript/state/storage.js", "../../../javascript/state/index.js", "../../../javascript/uuids.js", "../../../javascript/version.js", "../../../javascript/index.js"],
4
- "sourcesContent": ["const RESPONSE_HEADER = 'TurboBoost-Command'\n\nconst types = {\n boost: 'text/vnd.turbo-boost.html',\n stream: 'text/vnd.turbo-stream.html',\n html: 'text/html',\n xhtml: 'application/xhtml+xml',\n json: 'application/json'\n}\n\n// Prepares request headers for TurboBoost Command invocations\nconst prepare = (headers = {}) => {\n headers = { ...headers }\n\n // Assign Accept values\n const accepts = (headers['Accept'] || '')\n .split(',')\n .map(val => val.trim())\n .filter(val => val.length)\n\n accepts.unshift(types.boost, types.stream, types.html, types.xhtml)\n headers['Accept'] = [...new Set(accepts)].join(', ')\n\n // Assign Content-Type (Commands POST JSON via fetch/XHR)\n headers['Content-Type'] = types.json\n\n // Assign X-Requested-With for XHR detection\n headers['X-Requested-With'] = 'XMLHttpRequest'\n\n return headers\n}\n\n// Tokenizes the 'TurboBoost-Command' HTTP response header value\nconst tokenize = value => {\n if (value) {\n const [name, status, strategy] = value.split(', ')\n return { name, status, strategy }\n }\n\n return {}\n}\n\nexport default { prepare, tokenize, RESPONSE_HEADER }\n", "const append = content => {\n document.body.insertAdjacentHTML('beforeend', content)\n}\n\nconst replace = content => {\n const parser = new DOMParser()\n const doc = parser.parseFromString(content, 'text/html')\n const head = document.querySelector('head')\n const body = document.querySelector('body')\n const newHead = doc.querySelector('head')\n const newBody = doc.querySelector('body')\n if (head && newHead) TurboBoost?.Streams?.morph?.method(head, newHead)\n if (body && newBody) TurboBoost?.Streams?.morph?.method(body, newBody)\n}\n\n// TODO: dispatch events after append/replace so we can apply page state\nexport const render = (strategy, content) => {\n if (strategy && content) {\n if (strategy.match(/^Append$/i)) return append(content)\n if (strategy.match(/^Replace$/i)) return replace(content)\n }\n}\n\nexport default { render }\n", "import headers from './headers'\nimport { render } from './renderer'\n\nconst frameSources = {}\n\n// fires after receiving a turbo HTTP response\naddEventListener('turbo:before-fetch-response', event => {\n const frame = event.target.closest('turbo-frame')\n if (frame?.id && frame?.src) frameSources[frame.id] = frame.src\n\n const { fetchResponse: response } = event.detail\n const header = response.header(headers.RESPONSE_HEADER)\n\n if (!header) return\n\n // We'll take it from here Hotwire...\n event.preventDefault()\n const { strategy } = headers.tokenize(header)\n response.responseHTML.then(content => render(strategy, content))\n})\n\n// fires when a frame element is navigated and finishes loading\naddEventListener('turbo:frame-load', event => {\n const frame = event.target.closest('turbo-frame')\n frame.dataset.src = frameSources[frame.id] || frame.src || frame.dataset.src\n delete frameSources[frame.id]\n})\n", "const schema = {\n // attributes\n frameAttribute: 'data-turbo-frame',\n methodAttribute: 'data-turbo-method',\n commandAttribute: 'data-turbo-command',\n confirmAttribute: 'data-turbo-confirm',\n stateAttributesAttribute: 'data-turbo-boost-state-attributes'\n}\n\nexport default { ...schema }\n", "export const commandEvents = {\n start: 'turbo-boost:command:start',\n success: 'turbo-boost:command:success',\n finish: 'turbo-boost:command:finish',\n abort: 'turbo-boost:command:abort',\n clientError: 'turbo-boost:command:client-error',\n serverError: 'turbo-boost:command:server-error'\n}\n\nexport const stateEvents = {\n stateChange: 'turbo-boost:state:change',\n stateInitialize: 'turbo-boost:state:initialize'\n}\n\nexport const turboEvents = {\n frameLoad: 'turbo:frame-load',\n load: 'turbo:load'\n}\n\nexport function dispatch(name, target, options = {}) {\n return new Promise(resolve => {\n options = options || {}\n options.detail = options.detail || {}\n target = target || document\n const evt = new CustomEvent(name, { ...options, bubbles: true })\n target.dispatchEvent(evt)\n resolve(evt)\n })\n}\n", "const active = {}\n\nfunction add(payload) {\n active[payload.id] = payload\n}\n\nfunction remove(id) {\n delete active[id]\n}\n\nexport default {\n add,\n remove,\n get commands() {\n return [...Object.values(active)]\n },\n get length() {\n return Object.keys(active).length\n }\n}\n", "import { commandEvents } from './events'\nimport schema from './schema'\n\nconst confirmation = {\n method: message => Promise.resolve(confirm(message))\n}\n\nconst isTurboMethod = event => event.detail.driver === 'method'\n\nconst isTurboForm = event => {\n if (event.detail.driver !== 'form') return false\n\n const element = event.target\n const frame = element.closest('turbo-frame')\n const target = element.closest(`[${schema.frameAttribute}]`)\n return !!(frame || target)\n}\n\nconst shouldDelegate = event => isTurboMethod(event) || isTurboForm(event)\n\ndocument.addEventListener(commandEvents.start, async event => {\n const message = event.target.getAttribute(schema.confirmAttribute)\n if (!message) return\n\n event.detail.confirmation = true\n\n if (shouldDelegate(event)) return // delegate confirmation handling to Turbo\n\n const proceed = await confirmation.method(message)\n if (!proceed) event.preventDefault()\n})\n\nexport default confirmation\n", "let events = []\nlet eventListener\n\nfunction register(eventName, selectors) {\n const match = events.find(evt => evt.name === eventName)\n if (match) events.splice(events.indexOf(match), 1)\n events = [{ name: eventName, selectors }, ...events]\n\n document.removeEventListener(eventName, eventListener, true)\n document.addEventListener(eventName, eventListener, true)\n\n return { ...events.find(evt => evt.name === eventName) }\n}\n\nfunction getRegisteredEventForElement(element) {\n return events.find(evt =>\n evt.selectors.find(selector => Array.from(document.querySelectorAll(selector)).find(el => el === element))\n )\n}\n\nfunction isRegisteredForElement(eventName, element) {\n const evt = getRegisteredEventForElement(element)\n return evt && evt.name === eventName\n}\n\nexport default {\n register,\n isRegisteredForElement,\n get events() {\n return [...events]\n },\n set handler(fn) {\n eventListener = fn\n }\n}\n", "import schema from './schema'\n\nfunction findClosestCommand(element) {\n return element.closest(`[${schema.commandAttribute}]`)\n}\n\nfunction findClosestFrameWithSource(element) {\n return (\n element.closest('turbo-frame[src]') ||\n element.closest('turbo-frame[data-turbo-frame-src]') ||\n element.closest('turbo-frame')\n )\n}\n\nfunction assignElementValueToPayload(element, payload = {}) {\n if (element.tagName.toLowerCase() !== 'select') return (payload.value = element.value || null)\n\n if (!element.multiple) return (payload.value = element.options[element.selectedIndex].value)\n\n payload.values = Array.from(element.options).reduce((memo, option) => {\n if (option.selected) memo.push(option.value)\n return memo\n }, [])\n}\n\nfunction buildAttributePayload(element) {\n const payload = Array.from(element.attributes).reduce((memo, attr) => {\n let value = attr.value\n memo[attr.name] = value\n return memo\n }, {})\n\n payload.tag = element.tagName\n payload.checked = !!element.checked\n payload.disabled = !!element.disabled\n assignElementValueToPayload(element, payload)\n\n // reduce payload size to keep URL length smaller\n delete payload.class\n delete payload.action\n delete payload.href\n delete payload[schema.commandAttribute]\n delete payload[schema.frameAttribute]\n\n return payload\n}\n\nexport default {\n buildAttributePayload,\n findClosestCommand,\n findClosestFrameWithSource\n}\n", "const invokeCommand = (form, payload = {}) => {\n const input = form.querySelector('input[name=\"turbo_boost_command\"]') || document.createElement('input')\n input.type = 'hidden'\n input.name = 'turbo_boost_command'\n input.value = JSON.stringify(payload)\n if (!form.contains(input)) form.appendChild(input)\n}\n\nexport default { invokeCommand }\n", "import activity from './activity'\nimport { dispatch, commandEvents } from './events'\n\nfunction finish(event) {\n setTimeout(() => dispatch(commandEvents.finish, event.target, { detail: event.detail }))\n}\n\nconst events = [commandEvents.abort, commandEvents.serverError, commandEvents.success]\nevents.forEach(name => addEventListener(name, finish))\naddEventListener(commandEvents.finish, event => activity.remove(event.detail.id), true)\n\nexport default { events: commandEvents }\n", "const buildURL = path => {\n const a = document.createElement('a')\n a.href = path\n return new URL(a)\n}\n\nexport default {\n get commandInvocationURL() {\n return buildURL('/turbo-boost-command-invocation')\n }\n}\n", "import headers from './headers'\nimport lifecycle from './lifecycle'\nimport urls from './urls'\nimport { dispatch } from './events'\nimport { render } from './renderer'\n\nconst parseError = error => {\n const message = `Unexpected error performing a TurboBoost Command! ${error.message}`\n dispatch(lifecycle.events.clientError, document, { detail: { message, error } }, true)\n}\n\nconst parseAndRenderResponse = response => {\n const { strategy } = headers.tokenize(response.headers.get(headers.RESPONSE_HEADER))\n response.text().then(content => render(strategy, content))\n}\n\nconst invoke = (payload = {}) => {\n try {\n fetch(urls.commandInvocationURL.href, {\n method: 'POST',\n headers: headers.prepare({}),\n body: JSON.stringify(payload)\n })\n .then(parseAndRenderResponse)\n .catch(parseError)\n } catch (error) {\n parseError(error)\n }\n}\n\nexport { invoke }\n", "import { invoke } from '../invoker'\n\nconst invokeCommand = (_, payload) => invoke(payload)\n\nexport default { invokeCommand }\n", "let activeElement\nlet activePayload\n\nconst reset = () => {\n activeElement = null\n activePayload = null\n}\n\nconst invokeCommand = (element, payload = {}) => {\n activeElement = element\n activePayload = payload\n}\n\nconst amendForm = form => {\n try {\n if (!activeElement) return\n if (form.getAttribute('method') !== activeElement.dataset.turboMethod) return\n if (form.getAttribute('action') !== activeElement.href) return\n\n const input = form.querySelector('input[name=\"turbo_boost_command\"]') || document.createElement('input')\n input.type = 'hidden'\n input.name = 'turbo_boost_command'\n input.value = JSON.stringify(activePayload)\n if (!form.contains(input)) form.appendChild(input)\n } finally {\n reset() // ensure reset\n }\n}\n\ndocument.addEventListener('submit', event => amendForm(event.target), true)\n\nexport default { invokeCommand }\n", "import { invoke } from '../invoker'\n\nconst invokeCommand = (_, payload = {}) => invoke(payload)\n\nexport default { invokeCommand }\n", "import elements from '../elements'\nimport formDriver from './form'\nimport frameDriver from './frame'\nimport methodDriver from './method'\nimport windowDriver from './window'\n\nfunction src(element, frame) {\n frame = frame || { dataset: {} }\n return element.href || frame.src || frame.dataset.src || location.href\n}\n\nfunction find(element) {\n let frame = elements.findClosestFrameWithSource(element)\n\n const { turboFrame, turboMethod } = element.dataset\n\n if (element.tagName.toLowerCase() === 'form')\n return {\n name: 'form',\n reason: 'Element is a form.',\n frame,\n src: element.action,\n invokeCommand: formDriver.invokeCommand\n }\n\n if (turboMethod?.length)\n return {\n name: 'method',\n reason: 'Element defines data-turbo-method.',\n frame,\n src: element.href,\n invokeCommand: methodDriver.invokeCommand\n }\n\n // element targets a frame that is not _self\n if (turboFrame && turboFrame !== '_self') {\n frame = document.getElementById(turboFrame)\n return {\n name: 'frame',\n reason: 'element targets a frame that is not _self',\n frame,\n src: src(element, frame),\n invokeCommand: frameDriver.invokeCommand\n }\n }\n\n // element does NOT target a frame or targets _self and is contained by a frame\n if ((!turboFrame || turboFrame === '_self') && frame)\n return {\n name: 'frame',\n reason: 'element does NOT target a frame or targets _self and is contained by a frame',\n frame,\n src: src(element, frame),\n invokeCommand: frameDriver.invokeCommand\n }\n\n // element matches one or more of the following conditions\n // - targets _top\n // - does NOT target a frame\n // - is NOT contained by a frame\n return {\n name: 'window',\n reason:\n 'element matches one or more of the following conditions (targets _top, does NOT target a frame, is NOT contained by a frame)',\n frame: null,\n src: src(element),\n invokeCommand: windowDriver.invokeCommand\n }\n}\n\nexport default { find }\n", "// TODO: Move Logger to its own library (i.e. TurboBoost.Logger)\nimport { commandEvents as events } from './events'\n\nlet currentLevel = 'unknown'\nlet initialized = false\nlet history = []\n\nconst logLevels = {\n debug: Object.values(events),\n info: Object.values(events),\n warn: [events.abort, events.clientError, events.serverError],\n error: [events.clientError, events.serverError],\n unknown: []\n}\n\nconst shouldLogEvent = event => {\n if (!logLevels[currentLevel].includes(event.type)) return false\n if (typeof console[currentLevel] !== 'function') return false\n\n const { detail } = event\n if (!detail.id) return true\n\n const key = `${event.type}-${detail.id}`\n if (history.includes(key)) return false\n\n if (history.length > 16) history.shift()\n history.push(key)\n\n return true\n}\n\nconst logMethod = event => {\n if (logLevels.error.includes(event.type)) return 'error'\n if (logLevels.warn.includes(event.type)) return 'warn'\n if (logLevels.info.includes(event.type)) return 'info'\n if (logLevels.debug.includes(event.type)) return 'debug'\n return 'log'\n}\n\nconst logEvent = event => {\n if (shouldLogEvent(event)) {\n const { target, type, detail } = event\n const id = detail.id || ''\n const commandName = detail.name || ''\n\n let duration = ''\n if (detail.startedAt) duration = `${Date.now() - detail.startedAt}ms `\n\n const typeParts = type.split(':')\n const lastPart = typeParts.pop()\n const eventName = `%c${typeParts.join(':')}:%c${lastPart}`\n const message = [`%c${commandName}`, `%c${duration}`, eventName]\n\n console[logMethod(event)](\n message.join(' ').replace(/\\s{2,}/g, ' '),\n 'color:deepskyblue',\n 'color:lime',\n 'color:darkgray',\n eventName.match(/abort|error/i) ? 'color:red' : 'color:deepskyblue',\n { id, detail, target }\n )\n }\n}\n\nif (!initialized) {\n initialized = true\n Object.values(events).forEach(name => addEventListener(name, event => logEvent(event)))\n}\n\nexport default {\n get level() {\n return currentLevel\n },\n set level(value) {\n if (!Object.keys(logLevels).includes(value)) value = 'unknown'\n return (currentLevel = value)\n }\n}\n", "import { dispatch, stateEvents as events } from '../events'\n\nlet head\n\nfunction observable(object, parent = null) {\n if (!object || typeof object !== 'object') return object\n\n const proxy = new Proxy(object, {\n deleteProperty(target, key) {\n delete target[key]\n dispatch(events.stateChange, document, { detail: { state: head } })\n return true\n },\n\n set(target, key, value, _receiver) {\n target[key] = observable(value, this)\n dispatch(events.stateChange, document, { detail: { state: head } })\n return true\n }\n })\n\n if (Array.isArray(object)) {\n object.forEach((value, index) => (object[index] = observable(value, proxy)))\n } else if (typeof object === 'object') {\n for (const [key, value] of Object.entries(object)) object[key] = observable(value, proxy)\n }\n\n if (!parent) head = proxy\n return proxy\n}\n\nexport default observable\n", "import schema from '../schema.js'\n\nconst updateElement = (id, attribute, value, attempts = 1) => {\n if (attempts > 20) return\n const element = document.getElementById(id)\n if (element?.isConnected) return element.setAttribute(attribute, value)\n setTimeout(() => updateElement(id, attribute, value, attempts + 1), attempts * 5)\n}\n\nconst buildState = () => {\n const elements = Array.from(document.querySelectorAll(`[id][${schema.stateAttributesAttribute}]`))\n return elements.reduce((memo, element) => {\n const attributes = JSON.parse(element.getAttribute(schema.stateAttributesAttribute))\n if (element.id) {\n const stateAttributes = attributes.reduce((acc, name) => {\n if (element.hasAttribute(name)) acc[name] = element.getAttribute(name) || name\n return acc\n }, {})\n if (Object.values(stateAttributes).length) memo[element.id] = stateAttributes\n }\n return memo\n }, {})\n}\n\nconst restoreState = (state = {}) => {\n for (const [id, attributes] of Object.entries(state)) {\n for (const [attribute, value] of Object.entries(attributes)) updateElement(id, attribute, value)\n }\n}\n\nexport default {\n buildState,\n restoreState\n}\n", "function save(name, value) {\n if (typeof value !== 'object') value = {}\n return localStorage.setItem(String(name), JSON.stringify(value))\n}\n\nfunction find(name) {\n const stored = localStorage.getItem(String(name))\n return stored ? JSON.parse(stored) : {}\n}\n\nexport default { save, find }\n", "// TODO: Move State to its own library\nimport observable from './observable'\nimport page from './page'\nimport storage from './storage'\nimport { dispatch, stateEvents } from '../events'\n\nconst key = 'TurboBoost::State'\nconst stub = { pages: {}, signed: null, unsigned: {} }\n\nlet signed = null // signed state <string>\nlet unsigned = {} // unsigned state (optimistic) <object>\n\nconst restore = () => {\n const saved = { ...stub, ...storage.find(key) }\n signed = saved.signed\n unsigned = observable(saved.unsigned)\n saved.pages[location.pathname] = saved.pages[location.pathname] || {}\n page.restoreState(saved.pages[location.pathname])\n}\n\nconst save = () => {\n const saved = { ...stub, ...storage.find(key) }\n const fresh = {\n signed: signed || saved.signed,\n unsigned: { ...saved.unsigned, ...unsigned },\n pages: { ...saved.pages }\n }\n\n // update the current page's state entry\n const pageKey = location.pathname\n const pageState = page.buildState()\n Object.values(pageState).length ? (fresh.pages[pageKey] = pageState) : delete fresh.pages[pageKey]\n\n storage.save(key, fresh)\n}\n\nconst initialize = json => {\n const state = { ...stub, ...JSON.parse(json) }\n signed = state.signed\n unsigned = observable(state.unsigned)\n save()\n dispatch(stateEvents.stateInitialize, document, { detail: unsigned })\n}\n\n// setup\naddEventListener('DOMContentLoaded', restore)\naddEventListener('turbo:morph', restore)\naddEventListener('turbo:render', restore)\naddEventListener('turbo:before-fetch-request', save)\naddEventListener('beforeunload', save)\n\nexport default {\n initialize,\n buildPageState: page.buildState,\n get signed() {\n return signed\n },\n get unsigned() {\n return unsigned\n }\n}\n", "function v4() {\n return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>\n (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)\n )\n}\n\nexport default { v4 }\n", "export default '0.3.1'\n", "import './turbo'\nimport schema from './schema'\nimport { dispatch, commandEvents } from './events'\nimport activity from './activity'\nimport confirmation from './confirmation'\nimport delegates from './delegates'\nimport drivers from './drivers'\nimport elements from './elements'\nimport './lifecycle'\nimport logger from './logger'\nimport state from './state'\nimport uuids from './uuids'\nimport VERSION from './version'\n\nconst TurboBoost = self.TurboBoost || {}\n\nconst Commands = {\n VERSION,\n active: false,\n confirmation,\n logger,\n schema,\n events: commandEvents,\n registerEventDelegate: delegates.register,\n get eventDelegates() {\n return delegates.events\n }\n}\n\nfunction buildCommandPayload(id, element) {\n return {\n csrfToken: document.querySelector('meta[name=\"csrf-token\"]')?.getAttribute('content'), // -- Rails CSRF token\n id, //-------------------------------------------------------------------------------------- Uniquely identifies the command invocation\n name: element.getAttribute(schema.commandAttribute), //------------------------------------- Command name\n elementId: element.id.length ? element.id : null, //---------------------------------------- ID of the element that triggered the command\n elementAttributes: elements.buildAttributePayload(element), //------------------------------ Attributes of the element that triggered the command\n startedAt: Date.now(), //------------------------------------------------------------------- Start time of when the command was invoked\n state: {\n page: state.buildPageState(),\n signed: state.signed,\n unsigned: state.unsigned\n }\n }\n}\n\nasync function invokeCommand(event) {\n let element\n let payload = {}\n\n try {\n element = elements.findClosestCommand(event.target)\n if (!element) return\n if (!delegates.isRegisteredForElement(event.type, element)) return\n\n const commandId = uuids.v4()\n let driver = drivers.find(element)\n let payload = {\n ...buildCommandPayload(commandId, element),\n driver: driver.name,\n frameId: driver.frame ? driver.frame.id : null,\n src: driver.src\n }\n\n const startEvent = await dispatch(commandEvents.start, element, {\n cancelable: true,\n detail: payload\n })\n\n if (startEvent.defaultPrevented || (startEvent.detail.confirmation && event.defaultPrevented))\n return dispatch(commandEvents.abort, element, {\n detail: {\n message: `An event handler for '${commandEvents.start}' prevented default behavior and blocked command invocation!`,\n source: startEvent\n }\n })\n\n // the element and thus the driver may have changed based on the start event handler(s)\n driver = drivers.find(element)\n payload = {\n ...buildCommandPayload(commandId, element),\n driver: driver.name,\n frameId: driver.frame ? driver.frame.id : null,\n src: driver.src\n }\n\n activity.add(payload)\n\n if (['frame', 'window'].includes(driver.name)) event.preventDefault()\n\n switch (driver.name) {\n case 'method':\n return driver.invokeCommand(element, payload)\n case 'form':\n return driver.invokeCommand(element, payload, event)\n case 'frame':\n return driver.invokeCommand(driver.frame, payload)\n case 'window':\n return driver.invokeCommand(self, payload)\n }\n } catch (error) {\n dispatch(commandEvents.clientError, element, {\n detail: { ...payload, error }\n })\n }\n}\n\nself.TurboBoost = { ...TurboBoost }\n\nif (!self.TurboBoost.Commands) {\n // wire things up and setup defaults for event delegation\n delegates.handler = invokeCommand\n delegates.register('click', [`[${schema.commandAttribute}]`])\n delegates.register('submit', [`form[${schema.commandAttribute}]`])\n delegates.register('toggle', [`details[${schema.commandAttribute}]`])\n delegates.register('change', [\n `input[${schema.commandAttribute}]`,\n `select[${schema.commandAttribute}]`,\n `textarea[${schema.commandAttribute}]`\n ])\n\n self.TurboBoost.Commands = Commands\n self.TurboBoost.State = {\n initialize: state.initialize,\n get current() {\n return state.unsigned\n }\n }\n}\n\nexport default Commands\n"],
4
+ "sourcesContent": ["const RESPONSE_HEADER = 'TurboBoost-Command'\n\nconst types = {\n boost: 'text/vnd.turbo-boost.html',\n stream: 'text/vnd.turbo-stream.html',\n html: 'text/html',\n xhtml: 'application/xhtml+xml',\n json: 'application/json'\n}\n\n// Prepares request headers for TurboBoost Command invocations\nconst prepare = (headers = {}) => {\n headers = { ...headers }\n\n // Assign Accept values\n const accepts = (headers['Accept'] || '')\n .split(',')\n .map(val => val.trim())\n .filter(val => val.length)\n\n accepts.unshift(types.boost, types.stream, types.html, types.xhtml)\n headers['Accept'] = [...new Set(accepts)].join(', ')\n\n // Assign Content-Type (Commands POST JSON via fetch/XHR)\n headers['Content-Type'] = types.json\n\n // Assign X-Requested-With for XHR detection\n headers['X-Requested-With'] = 'XMLHttpRequest'\n\n return headers\n}\n\n// Tokenizes the 'TurboBoost-Command' HTTP response header value\nconst tokenize = value => {\n if (value) {\n const [name, status, strategy] = value.split(', ')\n return { name, status, strategy }\n }\n\n return {}\n}\n\nexport default { prepare, tokenize, RESPONSE_HEADER }\n", "const append = content => {\n document.body.insertAdjacentHTML('beforeend', content)\n}\n\nconst replace = content => {\n const parser = new DOMParser()\n const doc = parser.parseFromString(content, 'text/html')\n const head = document.querySelector('head')\n const body = document.querySelector('body')\n const newHead = doc.querySelector('head')\n const newBody = doc.querySelector('body')\n if (head && newHead) TurboBoost?.Streams?.morph?.method(head, newHead)\n if (body && newBody) TurboBoost?.Streams?.morph?.method(body, newBody)\n}\n\n// TODO: dispatch events after append/replace so we can apply page state\nexport const render = (strategy, content) => {\n if (strategy && content) {\n if (strategy.match(/^Append$/i)) return append(content)\n if (strategy.match(/^Replace$/i)) return replace(content)\n }\n}\n\nexport default { render }\n", "import headers from './headers'\nimport { render } from './renderer'\n\nconst frameSources = {}\n\n// fires after receiving a turbo HTTP response\naddEventListener('turbo:before-fetch-response', event => {\n const frame = event.target.closest('turbo-frame')\n if (frame?.id && frame?.src) frameSources[frame.id] = frame.src\n\n const { fetchResponse: response } = event.detail\n const header = response.header(headers.RESPONSE_HEADER)\n\n if (!header) return\n\n // We'll take it from here Hotwire...\n event.preventDefault()\n const { strategy } = headers.tokenize(header)\n response.responseHTML.then(content => render(strategy, content))\n})\n\n// fires when a frame element is navigated and finishes loading\naddEventListener('turbo:frame-load', event => {\n const frame = event.target.closest('turbo-frame')\n frame.dataset.src = frameSources[frame.id] || frame.src || frame.dataset.src\n delete frameSources[frame.id]\n})\n", "const schema = {\n // attributes\n frameAttribute: 'data-turbo-frame',\n methodAttribute: 'data-turbo-method',\n commandAttribute: 'data-turbo-command',\n confirmAttribute: 'data-turbo-confirm',\n stateAttributesAttribute: 'data-turbo-boost-state-attributes'\n}\n\nexport default { ...schema }\n", "export const commandEvents = {\n start: 'turbo-boost:command:start',\n success: 'turbo-boost:command:success',\n finish: 'turbo-boost:command:finish',\n abort: 'turbo-boost:command:abort',\n clientError: 'turbo-boost:command:client-error',\n serverError: 'turbo-boost:command:server-error'\n}\n\nexport const stateEvents = {\n stateChange: 'turbo-boost:state:change',\n stateInitialize: 'turbo-boost:state:initialize'\n}\n\nexport const turboEvents = {\n frameLoad: 'turbo:frame-load',\n load: 'turbo:load'\n}\n\nexport function dispatch(name, target, options = {}) {\n return new Promise(resolve => {\n options = options || {}\n options.detail = options.detail || {}\n target = target || document\n const evt = new CustomEvent(name, { ...options, bubbles: true })\n target.dispatchEvent(evt)\n resolve(evt)\n })\n}\n", "const active = {}\n\nfunction add(payload) {\n active[payload.id] = payload\n}\n\nfunction remove(id) {\n delete active[id]\n}\n\nexport default {\n add,\n remove,\n get commands() {\n return [...Object.values(active)]\n },\n get length() {\n return Object.keys(active).length\n }\n}\n", "import { commandEvents } from './events'\nimport schema from './schema'\n\nconst confirmation = {\n method: message => Promise.resolve(confirm(message))\n}\n\nconst isTurboMethod = event => event.detail.driver === 'method'\n\nconst isTurboForm = event => {\n if (event.detail.driver !== 'form') return false\n\n const element = event.target\n const frame = element.closest('turbo-frame')\n const target = element.closest(`[${schema.frameAttribute}]`)\n return !!(frame || target)\n}\n\nconst shouldDelegate = event => isTurboMethod(event) || isTurboForm(event)\n\ndocument.addEventListener(commandEvents.start, async event => {\n const message = event.target.getAttribute(schema.confirmAttribute)\n if (!message) return\n\n event.detail.confirmation = true\n\n if (shouldDelegate(event)) return // delegate confirmation handling to Turbo\n\n const proceed = await confirmation.method(message)\n if (!proceed) event.preventDefault()\n})\n\nexport default confirmation\n", "let events = []\nlet eventListener\n\nfunction register(eventName, selectors) {\n const match = events.find(evt => evt.name === eventName)\n if (match) events.splice(events.indexOf(match), 1)\n events = [{ name: eventName, selectors }, ...events]\n\n document.removeEventListener(eventName, eventListener, true)\n document.addEventListener(eventName, eventListener, true)\n\n return { ...events.find(evt => evt.name === eventName) }\n}\n\nfunction getRegisteredEventForElement(element) {\n return events.find(evt =>\n evt.selectors.find(selector => Array.from(document.querySelectorAll(selector)).find(el => el === element))\n )\n}\n\nfunction isRegisteredForElement(eventName, element) {\n const evt = getRegisteredEventForElement(element)\n return evt && evt.name === eventName\n}\n\nexport default {\n register,\n isRegisteredForElement,\n get events() {\n return [...events]\n },\n set handler(fn) {\n eventListener = fn\n }\n}\n", "import schema from './schema'\n\nfunction findClosestCommand(element) {\n return element.closest(`[${schema.commandAttribute}]`)\n}\n\nfunction findClosestFrameWithSource(element) {\n return (\n element.closest('turbo-frame[src]') ||\n element.closest('turbo-frame[data-turbo-frame-src]') ||\n element.closest('turbo-frame')\n )\n}\n\nfunction assignElementValueToPayload(element, payload = {}) {\n if (element.tagName.toLowerCase() !== 'select') return (payload.value = element.value || null)\n\n if (!element.multiple) return (payload.value = element.options[element.selectedIndex].value)\n\n payload.values = Array.from(element.options).reduce((memo, option) => {\n if (option.selected) memo.push(option.value)\n return memo\n }, [])\n}\n\nfunction buildAttributePayload(element) {\n const payload = Array.from(element.attributes).reduce((memo, attr) => {\n let value = attr.value\n memo[attr.name] = value\n return memo\n }, {})\n\n payload.tag = element.tagName\n payload.checked = !!element.checked\n payload.disabled = !!element.disabled\n assignElementValueToPayload(element, payload)\n\n // reduce payload size to keep URL length smaller\n delete payload.class\n delete payload.action\n delete payload.href\n delete payload[schema.commandAttribute]\n delete payload[schema.frameAttribute]\n\n return payload\n}\n\nexport default {\n buildAttributePayload,\n findClosestCommand,\n findClosestFrameWithSource\n}\n", "const invokeCommand = (form, payload = {}) => {\n const input = form.querySelector('input[name=\"turbo_boost_command\"]') || document.createElement('input')\n input.type = 'hidden'\n input.name = 'turbo_boost_command'\n input.value = JSON.stringify(payload)\n if (!form.contains(input)) form.appendChild(input)\n}\n\nexport default { invokeCommand }\n", "import activity from './activity'\nimport { dispatch, commandEvents } from './events'\n\nfunction finish(event) {\n setTimeout(() => dispatch(commandEvents.finish, event.target, { detail: event.detail }))\n}\n\nconst events = [commandEvents.abort, commandEvents.serverError, commandEvents.success]\nevents.forEach(name => addEventListener(name, finish))\naddEventListener(commandEvents.finish, event => activity.remove(event.detail.id), true)\n\nexport default { events: commandEvents }\n", "const buildURL = path => {\n const a = document.createElement('a')\n a.href = path\n return new URL(a)\n}\n\nexport default {\n get commandInvocationURL() {\n return buildURL('/turbo-boost-command-invocation')\n }\n}\n", "import headers from './headers'\nimport lifecycle from './lifecycle'\nimport urls from './urls'\nimport { dispatch } from './events'\nimport { render } from './renderer'\n\nconst parseError = error => {\n const message = `Unexpected error performing a TurboBoost Command! ${error.message}`\n dispatch(lifecycle.events.clientError, document, { detail: { message, error } }, true)\n}\n\nconst parseAndRenderResponse = response => {\n const { strategy } = headers.tokenize(response.headers.get(headers.RESPONSE_HEADER))\n response.text().then(content => render(strategy, content))\n}\n\nconst invoke = (payload = {}) => {\n try {\n fetch(urls.commandInvocationURL.href, {\n method: 'POST',\n headers: headers.prepare({}),\n body: JSON.stringify(payload)\n })\n .then(parseAndRenderResponse)\n .catch(parseError)\n } catch (error) {\n parseError(error)\n }\n}\n\nexport { invoke }\n", "import { invoke } from '../invoker'\n\nconst invokeCommand = (_, payload) => invoke(payload)\n\nexport default { invokeCommand }\n", "let activeElement\nlet activePayload\n\nconst reset = () => {\n activeElement = null\n activePayload = null\n}\n\nconst invokeCommand = (element, payload = {}) => {\n activeElement = element\n activePayload = payload\n}\n\nconst amendForm = form => {\n try {\n if (!activeElement) return\n if (form.getAttribute('method') !== activeElement.dataset.turboMethod) return\n if (form.getAttribute('action') !== activeElement.href) return\n\n const input = form.querySelector('input[name=\"turbo_boost_command\"]') || document.createElement('input')\n input.type = 'hidden'\n input.name = 'turbo_boost_command'\n input.value = JSON.stringify(activePayload)\n if (!form.contains(input)) form.appendChild(input)\n } finally {\n reset() // ensure reset\n }\n}\n\ndocument.addEventListener('submit', event => amendForm(event.target), true)\n\nexport default { invokeCommand }\n", "import { invoke } from '../invoker'\n\nconst invokeCommand = (_, payload = {}) => invoke(payload)\n\nexport default { invokeCommand }\n", "import elements from '../elements'\nimport formDriver from './form'\nimport frameDriver from './frame'\nimport methodDriver from './method'\nimport windowDriver from './window'\n\nfunction src(element, frame) {\n frame = frame || { dataset: {} }\n return element.href || frame.src || frame.dataset.src || location.href\n}\n\nfunction find(element) {\n let frame = elements.findClosestFrameWithSource(element)\n\n const { turboFrame, turboMethod } = element.dataset\n\n if (element.tagName.toLowerCase() === 'form')\n return {\n name: 'form',\n reason: 'Element is a form.',\n frame,\n src: element.action,\n invokeCommand: formDriver.invokeCommand\n }\n\n if (turboMethod?.length)\n return {\n name: 'method',\n reason: 'Element defines data-turbo-method.',\n frame,\n src: element.href,\n invokeCommand: methodDriver.invokeCommand\n }\n\n // element targets a frame that is not _self\n if (turboFrame && turboFrame !== '_self') {\n frame = document.getElementById(turboFrame)\n return {\n name: 'frame',\n reason: 'element targets a frame that is not _self',\n frame,\n src: src(element, frame),\n invokeCommand: frameDriver.invokeCommand\n }\n }\n\n // element does NOT target a frame or targets _self and is contained by a frame\n if ((!turboFrame || turboFrame === '_self') && frame)\n return {\n name: 'frame',\n reason: 'element does NOT target a frame or targets _self and is contained by a frame',\n frame,\n src: src(element, frame),\n invokeCommand: frameDriver.invokeCommand\n }\n\n // element matches one or more of the following conditions\n // - targets _top\n // - does NOT target a frame\n // - is NOT contained by a frame\n return {\n name: 'window',\n reason:\n 'element matches one or more of the following conditions (targets _top, does NOT target a frame, is NOT contained by a frame)',\n frame: null,\n src: src(element),\n invokeCommand: windowDriver.invokeCommand\n }\n}\n\nexport default { find }\n", "// TODO: Move Logger to its own library (i.e. TurboBoost.Logger)\nimport { commandEvents as events } from './events'\n\nlet currentLevel = 'unknown'\nlet initialized = false\nlet history = []\n\nconst logLevels = {\n debug: Object.values(events),\n info: Object.values(events),\n warn: [events.abort, events.clientError, events.serverError],\n error: [events.clientError, events.serverError],\n unknown: []\n}\n\nconst shouldLogEvent = event => {\n if (!logLevels[currentLevel].includes(event.type)) return false\n if (typeof console[currentLevel] !== 'function') return false\n\n const { detail } = event\n if (!detail.id) return true\n\n const key = `${event.type}-${detail.id}`\n if (history.includes(key)) return false\n\n if (history.length > 16) history.shift()\n history.push(key)\n\n return true\n}\n\nconst logMethod = event => {\n if (logLevels.error.includes(event.type)) return 'error'\n if (logLevels.warn.includes(event.type)) return 'warn'\n if (logLevels.info.includes(event.type)) return 'info'\n if (logLevels.debug.includes(event.type)) return 'debug'\n return 'log'\n}\n\nconst logEvent = event => {\n if (shouldLogEvent(event)) {\n const { target, type, detail } = event\n const id = detail.id || ''\n const commandName = detail.name || ''\n\n let duration = ''\n if (detail.startedAt) duration = `${Date.now() - detail.startedAt}ms `\n\n const typeParts = type.split(':')\n const lastPart = typeParts.pop()\n const eventName = `%c${typeParts.join(':')}:%c${lastPart}`\n const message = [`%c${commandName}`, `%c${duration}`, eventName]\n\n console[logMethod(event)](\n message.join(' ').replace(/\\s{2,}/g, ' '),\n 'color:deepskyblue',\n 'color:lime',\n 'color:darkgray',\n eventName.match(/abort|error/i) ? 'color:red' : 'color:deepskyblue',\n { id, detail, target }\n )\n }\n}\n\nif (!initialized) {\n initialized = true\n Object.values(events).forEach(name => addEventListener(name, event => logEvent(event)))\n}\n\nexport default {\n get level() {\n return currentLevel\n },\n set level(value) {\n if (!Object.keys(logLevels).includes(value)) value = 'unknown'\n return (currentLevel = value)\n }\n}\n", "import { dispatch, stateEvents as events } from '../events'\n\nlet head\n\nfunction observable(object, parent = null) {\n if (!object || typeof object !== 'object') return object\n\n const proxy = new Proxy(object, {\n deleteProperty(target, key) {\n delete target[key]\n dispatch(events.stateChange, document, { detail: { state: head } })\n return true\n },\n\n set(target, key, value, _receiver) {\n target[key] = observable(value, this)\n dispatch(events.stateChange, document, { detail: { state: head } })\n return true\n }\n })\n\n if (Array.isArray(object)) {\n object.forEach((value, index) => (object[index] = observable(value, proxy)))\n } else if (typeof object === 'object') {\n for (const [key, value] of Object.entries(object)) object[key] = observable(value, proxy)\n }\n\n if (!parent) head = proxy\n return proxy\n}\n\nexport default observable\n", "import schema from '../schema.js'\n\nconst updateElement = (id, attribute, value, attempts = 1) => {\n if (attempts > 20) return\n const element = document.getElementById(id)\n if (element?.isConnected) return element.setAttribute(attribute, value)\n setTimeout(() => updateElement(id, attribute, value, attempts + 1), attempts * 5)\n}\n\nconst buildState = () => {\n const elements = Array.from(document.querySelectorAll(`[id][${schema.stateAttributesAttribute}]`))\n return elements.reduce((memo, element) => {\n const attributes = JSON.parse(element.getAttribute(schema.stateAttributesAttribute))\n if (element.id) {\n const stateAttributes = attributes.reduce((acc, name) => {\n if (element.hasAttribute(name)) acc[name] = element.getAttribute(name) || name\n return acc\n }, {})\n if (Object.values(stateAttributes).length) memo[element.id] = stateAttributes\n }\n return memo\n }, {})\n}\n\nconst restoreState = (state = {}) => {\n for (const [id, attributes] of Object.entries(state)) {\n for (const [attribute, value] of Object.entries(attributes)) updateElement(id, attribute, value)\n }\n}\n\nexport default {\n buildState,\n restoreState\n}\n", "function save(name, value) {\n if (typeof value !== 'object') value = {}\n return localStorage.setItem(String(name), JSON.stringify(value))\n}\n\nfunction find(name) {\n const stored = localStorage.getItem(String(name))\n return stored ? JSON.parse(stored) : {}\n}\n\nexport default { save, find }\n", "// TODO: Move State to its own library\nimport observable from './observable'\nimport page from './page'\nimport storage from './storage'\nimport { dispatch, stateEvents } from '../events'\n\nconst key = 'TurboBoost::State'\nconst stub = { pages: {}, signed: null, unsigned: {} }\n\nlet signed = null // signed state <string>\nlet unsigned = {} // unsigned state (optimistic) <object>\n\nconst restore = () => {\n const saved = { ...stub, ...storage.find(key) }\n signed = saved.signed\n unsigned = observable(saved.unsigned)\n saved.pages[location.pathname] = saved.pages[location.pathname] || {}\n page.restoreState(saved.pages[location.pathname])\n}\n\nconst save = () => {\n const saved = { ...stub, ...storage.find(key) }\n const fresh = {\n signed: signed || saved.signed,\n unsigned: { ...saved.unsigned, ...unsigned },\n pages: { ...saved.pages }\n }\n\n // update the current page's state entry\n const pageKey = location.pathname\n const pageState = page.buildState()\n Object.values(pageState).length ? (fresh.pages[pageKey] = pageState) : delete fresh.pages[pageKey]\n\n storage.save(key, fresh)\n}\n\nconst initialize = json => {\n const state = { ...stub, ...JSON.parse(json) }\n signed = state.signed\n unsigned = observable(state.unsigned)\n save()\n dispatch(stateEvents.stateInitialize, document, { detail: unsigned })\n}\n\n// setup\naddEventListener('DOMContentLoaded', restore)\naddEventListener('turbo:morph', restore)\naddEventListener('turbo:render', restore)\naddEventListener('turbo:before-fetch-request', save)\naddEventListener('beforeunload', save)\n\nexport default {\n initialize,\n buildPageState: page.buildState,\n get signed() {\n return signed\n },\n get unsigned() {\n return unsigned\n }\n}\n", "function v4() {\n return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>\n (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)\n )\n}\n\nexport default { v4 }\n", "export default '0.3.2'\n", "import './turbo'\nimport schema from './schema'\nimport { dispatch, commandEvents } from './events'\nimport activity from './activity'\nimport confirmation from './confirmation'\nimport delegates from './delegates'\nimport drivers from './drivers'\nimport elements from './elements'\nimport './lifecycle'\nimport logger from './logger'\nimport state from './state'\nimport uuids from './uuids'\nimport VERSION from './version'\n\nconst TurboBoost = self.TurboBoost || {}\n\nconst Commands = {\n VERSION,\n active: false,\n confirmation,\n logger,\n schema,\n events: commandEvents,\n registerEventDelegate: delegates.register,\n get eventDelegates() {\n return delegates.events\n }\n}\n\nfunction buildCommandPayload(id, element) {\n return {\n csrfToken: document.querySelector('meta[name=\"csrf-token\"]')?.getAttribute('content'), // -- Rails CSRF token\n id, //-------------------------------------------------------------------------------------- Uniquely identifies the command invocation\n name: element.getAttribute(schema.commandAttribute), //------------------------------------- Command name\n elementId: element.id.length ? element.id : null, //---------------------------------------- ID of the element that triggered the command\n elementAttributes: elements.buildAttributePayload(element), //------------------------------ Attributes of the element that triggered the command\n startedAt: Date.now(), //------------------------------------------------------------------- Start time of when the command was invoked\n state: {\n page: state.buildPageState(),\n signed: state.signed,\n unsigned: state.unsigned\n }\n }\n}\n\nasync function invokeCommand(event) {\n let element\n let payload = {}\n\n try {\n element = elements.findClosestCommand(event.target)\n if (!element) return\n if (!delegates.isRegisteredForElement(event.type, element)) return\n\n const commandId = uuids.v4()\n let driver = drivers.find(element)\n let payload = {\n ...buildCommandPayload(commandId, element),\n driver: driver.name,\n frameId: driver.frame ? driver.frame.id : null,\n src: driver.src\n }\n\n const startEvent = await dispatch(commandEvents.start, element, {\n cancelable: true,\n detail: payload\n })\n\n if (startEvent.defaultPrevented || (startEvent.detail.confirmation && event.defaultPrevented))\n return dispatch(commandEvents.abort, element, {\n detail: {\n message: `An event handler for '${commandEvents.start}' prevented default behavior and blocked command invocation!`,\n source: startEvent\n }\n })\n\n // the element and thus the driver may have changed based on the start event handler(s)\n driver = drivers.find(element)\n payload = {\n ...buildCommandPayload(commandId, element),\n driver: driver.name,\n frameId: driver.frame ? driver.frame.id : null,\n src: driver.src\n }\n\n activity.add(payload)\n\n if (['frame', 'window'].includes(driver.name)) event.preventDefault()\n\n switch (driver.name) {\n case 'method':\n return driver.invokeCommand(element, payload)\n case 'form':\n return driver.invokeCommand(element, payload, event)\n case 'frame':\n return driver.invokeCommand(driver.frame, payload)\n case 'window':\n return driver.invokeCommand(self, payload)\n }\n } catch (error) {\n dispatch(commandEvents.clientError, element, {\n detail: { ...payload, error }\n })\n }\n}\n\nself.TurboBoost = { ...TurboBoost }\n\nif (!self.TurboBoost.Commands) {\n // wire things up and setup defaults for event delegation\n delegates.handler = invokeCommand\n delegates.register('click', [`[${schema.commandAttribute}]`])\n delegates.register('submit', [`form[${schema.commandAttribute}]`])\n delegates.register('toggle', [`details[${schema.commandAttribute}]`])\n delegates.register('change', [\n `input[${schema.commandAttribute}]`,\n `select[${schema.commandAttribute}]`,\n `textarea[${schema.commandAttribute}]`\n ])\n\n self.TurboBoost.Commands = Commands\n self.TurboBoost.State = {\n initialize: state.initialize,\n get current() {\n return state.unsigned\n }\n }\n}\n\nexport default Commands\n"],
5
5
  "mappings": "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",
6
6
  "names": ["RESPONSE_HEADER", "types", "prepare", "headers", "__spreadValues", "accepts", "val", "tokenize", "value", "name", "status", "strategy", "headers_default", "append", "content", "replace", "_a", "_b", "_c", "_d", "doc", "head", "body", "newHead", "newBody", "render", "strategy", "frameSources", "event", "frame", "response", "header", "headers_default", "strategy", "content", "render", "schema", "schema_default", "__spreadValues", "commandEvents", "stateEvents", "dispatch", "name", "target", "options", "resolve", "evt", "__spreadProps", "__spreadValues", "active", "add", "payload", "remove", "id", "activity_default", "confirmation", "message", "isTurboMethod", "event", "isTurboForm", "element", "frame", "target", "schema_default", "shouldDelegate", "commandEvents", "confirmation_default", "events", "eventListener", "register", "eventName", "selectors", "match", "evt", "__spreadValues", "getRegisteredEventForElement", "element", "selector", "el", "isRegisteredForElement", "delegates_default", "fn", "findClosestCommand", "element", "schema_default", "findClosestFrameWithSource", "assignElementValueToPayload", "payload", "memo", "option", "buildAttributePayload", "attr", "value", "elements_default", "invokeCommand", "form", "payload", "input", "form_default", "finish", "event", "dispatch", "commandEvents", "events", "name", "activity_default", "lifecycle_default", "buildURL", "path", "a", "urls_default", "parseError", "error", "message", "dispatch", "lifecycle_default", "parseAndRenderResponse", "response", "strategy", "headers_default", "content", "render", "invoke", "payload", "urls_default", "invokeCommand", "_", "payload", "invoke", "frame_default", "activeElement", "activePayload", "reset", "invokeCommand", "element", "payload", "amendForm", "form", "input", "event", "method_default", "invokeCommand", "_", "payload", "invoke", "window_default", "src", "element", "frame", "find", "elements_default", "turboFrame", "turboMethod", "form_default", "method_default", "frame_default", "window_default", "drivers_default", "currentLevel", "initialized", "history", "logLevels", "commandEvents", "shouldLogEvent", "event", "detail", "key", "logMethod", "logEvent", "target", "type", "id", "commandName", "duration", "typeParts", "lastPart", "eventName", "message", "name", "logger_default", "value", "head", "observable", "object", "parent", "proxy", "target", "key", "dispatch", "stateEvents", "value", "_receiver", "index", "observable_default", "updateElement", "id", "attribute", "value", "attempts", "element", "buildState", "schema_default", "memo", "attributes", "stateAttributes", "acc", "name", "restoreState", "state", "page_default", "save", "name", "value", "find", "stored", "storage_default", "key", "stub", "signed", "unsigned", "restore", "saved", "__spreadValues", "storage_default", "observable_default", "page_default", "save", "fresh", "pageKey", "pageState", "initialize", "json", "state", "dispatch", "stateEvents", "state_default", "v4", "c", "uuids_default", "version_default", "TurboBoost", "Commands", "version_default", "confirmation_default", "logger_default", "schema_default", "commandEvents", "delegates_default", "buildCommandPayload", "id", "element", "_a", "elements_default", "state_default", "invokeCommand", "event", "payload", "commandId", "uuids_default", "driver", "drivers_default", "__spreadProps", "__spreadValues", "startEvent", "dispatch", "activity_default", "error", "javascript_default"]
7
7
  }
@@ -1 +1 @@
1
- export default '0.3.1'
1
+ export default '0.3.2'
@@ -21,8 +21,8 @@ class TurboBoost::Commands::AttributeSet
21
21
  name.delete_prefix!("#{prefix}_") unless prefix.blank?
22
22
 
23
23
  # type casting
24
- value = value.to_i if value.is_a?(String) && value.match?(/\A-?\d+\z/)
25
- value = value == "true" if value.is_a?(String) && value.match?(/\A(true|false)\z/i)
24
+ value = value.to_i if cast_to_integer?(value)
25
+ value = value == "true" if cast_to_boolean?(value)
26
26
 
27
27
  begin
28
28
  next if instance_variable_defined?(:"@#{name}")
@@ -75,4 +75,18 @@ class TurboBoost::Commands::AttributeSet
75
75
  return false if name.end_with?("?")
76
76
  nil
77
77
  end
78
+
79
+ private
80
+
81
+ def cast_to_integer?(value)
82
+ return false unless value.is_a?(String)
83
+ return false unless value.match?(/\A-?\d+\z/)
84
+ return false if value.size > 1 && value.start_with?("0")
85
+ true
86
+ end
87
+
88
+ def cast_to_boolean?(value)
89
+ return false unless value.is_a?(String)
90
+ value.match?(/\A(true|false)\z/i)
91
+ end
78
92
  end
@@ -25,9 +25,10 @@ module TurboBoost::Commands
25
25
  config.turbo_boost_commands[:alert_on_abort] = false # (true, false, "development", "test", "production")
26
26
  config.turbo_boost_commands[:alert_on_error] = false # (true, false, "development", "test", "production")
27
27
  config.turbo_boost_commands[:precompile_assets] = true # (true, false)
28
- config.turbo_boost_commands[:protect_from_forgery] = false # (true, false) TODO: Support override in Commands
28
+ config.turbo_boost_commands[:protect_from_forgery] = true # (true, false)
29
29
  config.turbo_boost_commands[:raise_on_invalid_command] = "development" # (true, false, "development", "test", "production")
30
30
  config.turbo_boost_commands[:resolve_state] = false # (true, false)
31
+ config.turbo_boost_commands[:verify_client] = true # (true, false)
31
32
 
32
33
  initializer "turbo_boost_commands.configuration", before: :build_middleware_stack do |app|
33
34
  Mime::Type.register "text/vnd.turbo-boost.html", :turbo_boost
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "device_detector"
4
+
3
5
  class TurboBoost::Commands::EntryMiddleware
4
6
  PATH = "/turbo-boost-command-invocation"
7
+ PARAM = "turbo_boost_command"
5
8
 
6
9
  def initialize(app)
7
10
  @app = app
@@ -9,23 +12,74 @@ class TurboBoost::Commands::EntryMiddleware
9
12
 
10
13
  def call(env)
11
14
  request = Rack::Request.new(env)
12
- modify! request if modify?(request)
15
+
16
+ # a command was not requested, pass through and exit early
17
+ return @app.call(env) unless command_request?(request)
18
+
19
+ # a command was requested
20
+ return [403, {"Content-Type" => "text/plain"}, ["Forbidden"]] if untrusted_client?(request)
21
+ modify_request!(request) if modify_request?(request)
13
22
  @app.call env
14
23
  end
15
24
 
16
25
  private
17
26
 
18
- # Returns the MIME type for TurboBoost Command invocations.
19
27
  def mime_type
20
- Mime::Type.lookup_by_extension(:turbo_boost)
28
+ @mime_type ||= Mime::Type.lookup_by_extension(:turbo_boost)
29
+ end
30
+
31
+ # Indicates if the client's user agent is trusted (i.e. known and not a bot)
32
+ #
33
+ # @param request [Rack::Request] the request to check
34
+ # @return [Boolean]
35
+ def trusted_client?(request)
36
+ return true unless TurboBoost::Commands.config.verify_client
37
+ client = DeviceDetector.new(request.env["HTTP_USER_AGENT"])
38
+ return false unless client.known?
39
+ return false if client.bot?
40
+ true
41
+ rescue => error
42
+ puts "#{self.class.name} failed to determine if the client is valid! #{error.message}"
43
+ false
44
+ end
45
+
46
+ # Indicates if the client's user agent is untrusted (i.e. unknown or a bot)
47
+ #
48
+ # @param request [Rack::Request] the request to check
49
+ # @return [Boolean]
50
+ def untrusted_client?(request)
51
+ !trusted_client?(request)
52
+ end
53
+
54
+ # Indicates if the request is invoking a TurboBoost Command.
55
+ #
56
+ # @param request [Rack::Request] the request to check
57
+ # @return [Boolean]
58
+ def command_request?(request)
59
+ return false unless request.post?
60
+ return false unless request.path.start_with?(PATH) || request.params.key?(PARAM)
61
+ true
62
+ end
63
+
64
+ # The TurboBoost Command params.
65
+ #
66
+ # @param request [Rack::Request] the request to extract the params from
67
+ # @return [Hash]
68
+ def command_params(request)
69
+ return {} unless command_request?(request)
70
+ return request.params[PARAM] if request.params.key?(PARAM)
71
+ JSON.parse(request.body.string)
21
72
  end
22
73
 
23
74
  # Indicates whether or not the request is a TurboBoost Command invocation that requires modifications
24
75
  # before we hand things over to Rails.
25
76
  #
77
+ # @note The form and method drivers DO NOT modify the request;
78
+ # instead, they let Rails mechanics handle the request as normal.
79
+ #
26
80
  # @param request [Rack::Request] the request to check
27
81
  # @return [Boolean] true if the request is a TurboBoost Command invocation, false otherwise
28
- def modify?(request)
82
+ def modify_request?(request)
29
83
  return false unless request.post?
30
84
  return false unless request.path.start_with?(PATH)
31
85
  return false unless mime_type && request.env["HTTP_ACCEPT"]&.include?(mime_type)
@@ -35,11 +89,6 @@ class TurboBoost::Commands::EntryMiddleware
35
89
  false
36
90
  end
37
91
 
38
- def convert_to_get_request?(driver)
39
- return true if driver == "frame" || driver == "window"
40
- false
41
- end
42
-
43
92
  # Modifies the given POST request so Rails sees it as GET.
44
93
  #
45
94
  # The posted JSON body content holds the TurboBoost Command meta data.
@@ -65,28 +114,42 @@ class TurboBoost::Commands::EntryMiddleware
65
114
  # }
66
115
  #
67
116
  # @param request [Rack::Request] the request to modify
68
- def modify!(request)
69
- params = JSON.parse(request.body.string)
117
+ def modify_request!(request)
118
+ params = command_params(request)
70
119
  uri = URI.parse(params["src"])
71
120
 
72
121
  request.env.tap do |env|
73
122
  # Store the command params in the environment
74
123
  env["turbo_boost_command_params"] = params
75
124
 
76
- # Update the URI, PATH_INFO, and QUERY_STRING
125
+ # Change URI and path
77
126
  env["REQUEST_URI"] = uri.to_s if env.key?("REQUEST_URI")
78
- env["PATH_INFO"] = uri.path
127
+ env["REQUEST_PATH"] = uri.path
128
+ env["PATH_INFO"] = begin
129
+ script_name = Rails.application.config.relative_url_root
130
+ path_info = uri.path.sub(/^#{Regexp.escape(script_name.to_s)}/, "")
131
+ path_info.empty? ? "/" : path_info
132
+ end
133
+
134
+ # Change query string
79
135
  env["QUERY_STRING"] = uri.query.to_s
136
+ env.delete("rack.request.query_hash")
80
137
 
81
- # Change the method from POST to GET
82
- if convert_to_get_request?(params["driver"])
83
- env["REQUEST_METHOD"] = "GET"
138
+ # Clear form data
139
+ env.delete("rack.request.form_input")
140
+ env.delete("rack.request.form_hash")
141
+ env.delete("rack.request.form_vars")
142
+ env.delete("rack.request.form_pairs")
84
143
 
85
- # Clear the body and related headers so the appears and behaves like a GET
86
- env["rack.input"] = StringIO.new
87
- env["CONTENT_LENGTH"] = "0"
88
- env.delete("CONTENT_TYPE")
89
- end
144
+ # Clear the body so we can change the the method to GET
145
+ env["rack.input"] = StringIO.new
146
+ env["CONTENT_LENGTH"] = "0"
147
+ env["content-length"] = "0"
148
+ env.delete("CONTENT_TYPE")
149
+ env.delete("content-type")
150
+
151
+ # Change the method to GET
152
+ env["REQUEST_METHOD"] = "GET"
90
153
  end
91
154
  rescue => error
92
155
  puts "#{self.class.name} failed to modify the request! #{error.message}"
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TurboBoost::Commands::ExitMiddleware
4
- BODY_PATTERN = /<\/\s*body/io
5
- TURBO_FRAME_PATTERN = /<\/\s*turbo-frame/io
6
- TURBO_STREAM_PATTERN = /<\/\s*turbo-stream/io
7
- TAIL_PATTERN = /\z/io
4
+ BODY_PATTERN = /<\/\s*body/i
5
+ TURBO_FRAME_PATTERN = /<\/\s*turbo-frame/i
6
+ TURBO_STREAM_PATTERN = /<\/\s*turbo-stream/i
7
+ TAIL_PATTERN = /\z/i
8
8
 
9
9
  def initialize(app)
10
10
  @app = app
@@ -6,7 +6,7 @@ module TurboBoost::Commands::Patches::ActionViewHelpersTagHelperTagBuilderPatch
6
6
  def tag_options(options, ...)
7
7
  options = turbo_boost&.state&.tag_options(options) || options
8
8
  options = TurboBoost::Commands::AttributeHydration.dehydrate(options)
9
- super(options, ...)
9
+ super
10
10
  end
11
11
 
12
12
  private
@@ -36,9 +36,8 @@ class TurboBoost::Commands::TokenValidator
36
36
  def tokens
37
37
  list = Set.new.tap do |set|
38
38
  set.add command.params[:csrf_token]
39
-
40
- # TODO: Update to use Rails' public API
41
- set.merge controller.send(:request_authenticity_tokens)
39
+ set.add controller.request.x_csrf_token
40
+ set.add controller.params[controller.class.request_forgery_protection_token]
42
41
  end
43
42
 
44
43
  list.select(&:present?).to_a
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TurboBoost
4
4
  module Commands
5
- VERSION = "0.3.1"
5
+ VERSION = "0.3.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo_boost-commands
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nate Hopkins (hopsoft)
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-30 00:00:00.000000000 Z
11
+ date: 2024-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: device_detector
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: observer
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: rails
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -388,20 +416,6 @@ dependencies:
388
416
  - - ">="
389
417
  - !ruby/object:Gem::Version
390
418
  version: '0'
391
- - !ruby/object:Gem::Dependency
392
- name: webdrivers
393
- requirement: !ruby/object:Gem::Requirement
394
- requirements:
395
- - - ">="
396
- - !ruby/object:Gem::Version
397
- version: '0'
398
- type: :development
399
- prerelease: false
400
- version_requirements: !ruby/object:Gem::Requirement
401
- requirements:
402
- - - ">="
403
- - !ruby/object:Gem::Version
404
- version: '0'
405
419
  description: Commands to help you build robust reactive applications with Rails &
406
420
  Hotwire.
407
421
  email:
@@ -494,7 +508,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
494
508
  - !ruby/object:Gem::Version
495
509
  version: '0'
496
510
  requirements: []
497
- rubygems_version: 3.5.10
511
+ rubygems_version: 3.5.9
498
512
  signing_key:
499
513
  specification_version: 4
500
514
  summary: Commands to help you build robust reactive applications with Rails & Hotwire.