flash_unified 0.1.0 → 0.3.0

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: 7063ee3fc735eb8fcf88ca5562119d948fc68464e2c7be0696b1e7fbc1542a87
4
- data.tar.gz: bdddb50bb03254d0d1ddf07d18dae37d48a4623d02a2f7bed83243e3c6920ca4
3
+ metadata.gz: 1ff9e758b822e2e06c38f1543fc5e2be3b4aa101879bdd1137ab70f01b0cc350
4
+ data.tar.gz: 1df992f44cd7c2db0992b4130d9047af5c7017a26e3367aa395597a884adbddb
5
5
  SHA512:
6
- metadata.gz: 399bbe669d11e36909ea54d022ea1365c8f69cccad30ae3d2f2d8e1535e927f204dbcc05e32e184553bb718b5496cfec12d72e79c28bc4c4f4d210af1df8777a
7
- data.tar.gz: 133059a7460e3cff955b47a187ce42f27300e87774a2890b174a0597a729d7d2c6ea1b5ccf425fc17853183a3a822c9469978f41894d37005f556fcb8ecb71aa
6
+ metadata.gz: 1f327a2f3b87d9974b81b1031f5d787cc6b3f03927398030e0e89fb39e8c0e94232cc6ec86195dbe6e56d53a1cb140d07bb5c52d803a34d1f7930c60584aeacc
7
+ data.tar.gz: fcd91ea10bdeccefc552f5c66e6c6e8e4ddc053acb4f3e638b1c3b8e04db51efd6463478464cc7e308e38bbc7db54fb266ccf21ee7df3782569ffe6664abc575
data/README.md CHANGED
@@ -2,29 +2,28 @@
2
2
 
3
3
  FlashUnified provides a unified Flash message rendering mechanism for Rails applications that can be used from both server-side and client-side code.
4
4
 
5
- Server-side view helpers embed Flash messages into the page as data, and a lightweight client-side JavaScript reads those embeddings and renders them onto the page.
5
+ Server-side view helpers embed Flash messages as data in the page, and a lightweight client-side JavaScript library reads those storages and renders messages into visible containers using templates.
6
6
 
7
7
  ## Current status
8
8
 
9
- This project is considered alpha up to v1.0.0. Public APIs are not stable and may change in future releases.
9
+ This project is considered alpha through v1.0.0. Public APIs are not yet stable and may change in future releases.
10
10
 
11
11
  ## Motivation
12
12
 
13
- Two concerns motivated this work.
13
+ We had two challenges at the same time.
14
14
 
15
- One is to be able to present client-originated messages using the same UI as server-side Flash messages. For example, when a large request is blocked by a proxy and a 413 error occurs, the client must handle it because the request does not reach the Rails server; nevertheless we want to display it using the same Flash UI.
15
+ One was to display messages originating from the client-side with the same UI representation as Flash from the server-side. For example, when a large request is blocked by a proxy, we want to display a 413 error as a Flash message. Since the request never reaches the Rails server, this must be handled on the client-side, but we want to display it with the same UI logic as normal Flash.
16
16
 
17
- The other is to support showing Flash messages that originate from Turbo Frames. Displaying Flash inside a frame is straightforward, but in most applications messages are shown outside the frame.
17
+ The other was to display Flash messages from Turbo Frames as well. It's not a problem if Flash is displayed within the frame, but in most cases it will be displayed outside the frame.
18
18
 
19
19
  ## How it works
20
20
 
21
- The key is that rendering must be done on the JavaScript side. We split responsibilities between server and client into two steps:
21
+ The key point to solving these challenges is that rendering needs to be performed on the JavaScript side. Therefore, we devised a two-stage process that divides responsibilities between the server-side and client-side:
22
22
 
23
- 1. The server embeds the Flash object into the page as hidden DOM elements and returns the rendered page.
24
- 2. The client-side JavaScript detects page changes, scans those elements, reads the embedded messages, formats them using templates, and inserts them into the specified container element. After rendering, the message elements are removed from the DOM to avoid duplicate displays.
25
-
26
- This mechanism is simple; the main requirement is to agree on how to embed data. This gem defines a DOM structure for the embedding, which we refer to as "storage":
23
+ 1. The server embeds the Flash object as a hidden DOM element within the page, renders the page, and returns it.
24
+ 2. When the client-side JavaScript detects a page change, it scans those elements, reads the embedded messages, formats them using templates, and inserts (renders) them into the specified container elements. At that time, message elements are removed from the DOM to avoid duplicate displays.
27
25
 
26
+ The mechanism is simple, and to implement it, we only need to decide on the rules for how to embed. In this gem, we define the embedded DOM structure as follows and call it "storage":
28
27
  ```erb
29
28
  <div data-flash-storage style="display: none;">
30
29
  <ul>
@@ -35,16 +34,13 @@ This mechanism is simple; the main requirement is to agree on how to embed data.
35
34
  </div>
36
35
  ```
37
36
 
38
- Because storage is hidden, it can be placed anywhere in the rendered page. For Turbo Frames, place it inside the frame.
39
-
40
- The container (where Flash messages are displayed) and the templates used for formatting are independent of the storage location. In other words, even when storage is inside a Turbo Frame, the rendering can target a container outside the frame.
41
-
42
- For client-detected cases (for example, when a proxy returns 413 on form submission), instead of rendering an error message directly from JavaScript, embed the message into a container element and let the same templates and flow render it as a Flash message.
37
+ Since storage is a hidden element, it can be placed anywhere in the page rendered by the server. For Turbo Frames, place it inside the frame.
43
38
 
44
- ### Controller example
39
+ The "container" where Flash messages are displayed and the "templates" for formatting can be placed anywhere regardless of the storage. This means that even with Turbo Frames, it works with Flash rendering areas placed outside the frame.
45
40
 
46
- Controller-side procedures for setting Flash are unchanged:
41
+ When handling cases on the client-side where a proxy returns an error when a form is submitted, instead of displaying the error message directly from JavaScript, you can render Flash in the same way (using the same templates and processing flow) by temporarily embedding the message as a storage element.
47
42
 
43
+ On the other hand, in controllers that set Flash, there is no difference from the normal Flash message display procedure:
48
44
  ```ruby
49
45
  if @user.save
50
46
  redirect_to @user, notice: "Created successfully."
@@ -54,279 +50,71 @@ else
54
50
  end
55
51
  ```
56
52
 
57
- Introducing this gem does not require changes to existing controllers. Layout changes are minimal since storage elements are hidden; usually you only need to adjust the container area for Flash messages.
58
-
59
- The main implementation task when using this gem is deciding when the embedded data should be rendered as Flash messages. Typically this is done with events. The gem provides helpers that automatically set up event handlers, but you may call rendering methods directly where appropriate.
60
-
61
- ## Main features
62
-
63
- This gem provides rules for embedding data and helper tools for implementation.
53
+ In other words, when introducing this gem, **no changes are required to existing controllers**. Also, there is almost no need to change existing page layouts. The DOM elements to be set in views are hidden elements, and you only need to slightly adjust the container area where Flash messages are displayed.
64
54
 
65
- Server-side:
66
- - View helpers that render DOM fragments expected by the client:
67
- - Hidden storage elements for temporarily saving messages in the page
68
- - Templates for the actual display elements
69
- - A container element indicating where templates should be inserted
70
- - Localized messages for HTTP status (for advanced usage)
55
+ The main thing to implement when introducing this gem is the timing to display embedded data as Flash messages. Normally, you will use events. While the specific implementation is left to the implementer, helpers are provided to automatically set up events. You can also explicitly call methods for display within arbitrary processes.
71
56
 
72
- Client-side:
73
- - A minimal ES Module in `flash_unified.js`. Configure via Importmap or the asset pipeline.
74
- - `auto.js` for automatic initialization (optional)
75
- - `turbo_helpers.js` for Turbo integration (optional)
76
- - `network_helpers.js` for network/HTTP error display (optional)
57
+ ## Quick Start
77
58
 
78
- Generator:
79
- - An installer generator that copies the above files into the host application.
80
-
81
- ## Installation
82
-
83
- Add the following to your application's `Gemfile`:
59
+ ### 1. Installation
84
60
 
61
+ When using Bundler, add the gem entry to your Gemfile:
85
62
  ```ruby
86
63
  gem 'flash_unified'
87
64
  ```
88
65
 
89
- Then run:
90
-
66
+ Run the command:
91
67
  ```bash
92
68
  bundle install
93
69
  ```
94
70
 
95
- ## Setup
96
-
97
- ### 1. Place files
98
-
99
- Run the installer generator:
100
-
71
+ Or install it directly:
101
72
  ```bash
102
- bin/rails generate flash_unified:install
73
+ gem install flash_unified
103
74
  ```
104
75
 
105
- ### 2. Add JavaScript
106
-
107
- **Importmap:**
108
-
109
- `config/importmap.rb` should pin the JavaScript modules you use. The installer generator and sandbox templates pin all shipped modules, but if you manage pins manually, include at least the following:
76
+ ### 2. Client-side setup (Importmap)
110
77
 
78
+ Add to `config/importmap.rb`:
111
79
  ```ruby
112
- pin "flash_unified/auto", to: "flash_unified/auto.js"
113
- pin "flash_unified", to: "flash_unified/flash_unified.js"
114
- pin "flash_unified/turbo_helpers", to: "flash_unified/turbo_helpers.js"
115
- pin "flash_unified/network_helpers", to: "flash_unified/network_helpers.js"
116
- ```
117
-
118
- If you want the library to set up rendering timing automatically, use `auto.js`. `auto.js` will register Turbo integration events, custom event listeners, and perform initial render handling automatically.
119
-
120
- If you prefer to control those events yourself, import the core `flash_unified` module and call the provided helpers (for example, `installInitialRenderListener()` for initial render, `installTurboRenderListeners()` from `flash_unified/turbo_helpers` for Turbo lifecycle hooks, and `installCustomEventListener()` to subscribe to `flash-unified:messages`). `turbo_helpers.js` and `network_helpers.js` are optional—pin only the ones you will use.
121
-
122
- **Asset pipeline (Propshaft / Sprockets):**
123
-
124
- This gem's JavaScript is provided as ES modules. Instead of `pin`ning, add the following to an appropriate place in your layout:
125
- ```erb
126
- <link rel="modulepreload" href="<%= asset_path('flash_unified/flash_unified.js') %>">
127
- <link rel="modulepreload" href="<%= asset_path('flash_unified/network_helpers.js') %>">
128
- <link rel="modulepreload" href="<%= asset_path('flash_unified/turbo_helpers.js') %>">
129
- <link rel="modulepreload" href="<%= asset_path('flash_unified/auto.js') %>">
130
- <script type="importmap">
131
- {
132
- "imports": {
133
- "flash_unified": "<%= asset_path('flash_unified/flash_unified.js') %>",
134
- "flash_unified/auto": "<%= asset_path('flash_unified/auto.js') %>",
135
- "flash_unified/turbo_helpers": "<%= asset_path('flash_unified/turbo_helpers.js') %>",
136
- "flash_unified/network_helpers": "<%= asset_path('flash_unified/network_helpers.js') %>"
137
- }
138
- }
139
- </script>
140
- <script type="module">
141
- import "flash_unified/auto";
142
- </script>
143
- ```
144
-
145
- ### 3. JavaScript initialization
146
-
147
- When using helpers, ensure the initialization that registers event handlers runs on page load.
148
-
149
- **Automatic (simple case):**
150
-
151
- Import `auto.js` in your JS entry (e.g. `app/javascript/application.js`):
152
-
153
- ```js
154
- import "flash_unified/auto";
155
- ```
156
-
157
- `auto.js` runs initialization when loaded. Its behavior can be controlled with data attributes on `<html>` (described below).
158
-
159
- **Semi-automatic (Turbo events are set automatically):**
160
-
161
- When using `turbo_helpers.js`, initialization is not run automatically after import. Call the provided functions:
162
-
163
- ```js
164
- import { installInitialRenderListener } from "flash_unified";
165
- import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
166
-
167
- installTurboRenderListeners();
168
- installInitialRenderListener();
169
- ```
170
-
171
- This ensures Flash messages are rendered when page changes occur (Turbo events).
172
-
173
- **Manual (implement your own handlers):**
174
-
175
- If you implement event registration yourself, at minimum call `renderFlashMessages()` on initial page load. A helper `installInitialRenderListener()` is provided for this purpose:
176
-
177
- ```js
178
- import { installInitialRenderListener } from "flash_unified";
179
- installInitialRenderListener();
180
- ```
181
-
182
- Decide an appropriate timing to call `renderFlashMessages()`—typically within an event handler.
183
-
184
- ## Server setup
185
-
186
- ### Helpers
187
-
188
- Server-side view helpers render the DOM fragments the client expects. Most partials do not need changes other than the `flash_templates` partial.
189
-
190
- - `flash_global_storage` — a global storage element (includes `id="flash-storage"`).
191
- - `flash_storage` — a storage element; include it inside the content you return.
192
- - `flash_templates` — templates (`<template>` elements) used by the client.
193
- - `flash_container` — the container where Flash messages are displayed.
194
- - `flash_general_error_messages` — a node with messages for HTTP status codes.
195
-
196
- Important: the JavaScript relies on specific DOM contracts (for example, a global storage element with `id="flash-storage"` and template IDs in the form `flash-message-template-<type>`). Changing these IDs/selectors without updating the JavaScript will break integration.
197
-
198
- ### Minimal layout example
199
-
200
- Storage elements can be placed anywhere. Typically they are included near the top of the body:
201
-
202
- ```erb
203
- <%= flash_general_error_messages %>
204
- <%= flash_global_storage %>
205
- <%= flash_templates %>
80
+ pin "flash_unified/all", to: "flash_unified/all.bundle.js"
206
81
  ```
207
82
 
208
- Place the visible container where users should see messages:
209
-
210
- ```erb
211
- <%= flash_container %>
83
+ Import in your JavaScript entry point (e.g., `app/javascript/application.js`):
84
+ ```javascript
85
+ import "flash_unified/all";
212
86
  ```
213
87
 
214
- Embed `flash_storage` inside the response content (for Turbo Frame responses, render it inside the frame):
88
+ ### 3. Server-side setup
215
89
 
90
+ Place the "sources" with the helper right after `<body>` in your layout:
216
91
  ```erb
217
- <%= flash_storage %>
92
+ <body>
93
+ <%= flash_unified_sources %>
94
+ ...
218
95
  ```
219
96
 
220
- ### Template customization
221
-
222
- To customize the appearance and markup of Flash messages, edit the partial template `app/views/flash_unified/_templates.html.erb` that is copied into your host Rails application by the installer generator.
223
-
224
- Here is a partial example:
225
-
97
+ Place the "container" with the helper at the location where you want to display messages:
226
98
  ```erb
227
- <template id="flash-message-template-notice">
228
- <div class="flash-notice" role="alert">
229
- <span class="flash-message-text"></span>
230
- </div>
231
- </template>
232
- <template id="flash-message-template-warning">
233
- <div class="flash-warning" role="alert">
234
- <span class="flash-message-text"></span>
235
- </div>
236
- </template>
99
+ <div class="notify">
100
+ <%= flash_container %>
101
+ ...
237
102
  ```
238
103
 
239
- Template IDs like `flash-message-template-notice` map to Flash types (`:notice`, `:alert`, `:warning`). The client inserts the message string into `.flash-message-text`. Otherwise the templates are free-form; add elements such as dismiss buttons as needed.
240
-
241
- ## JavaScript API and extensions
242
-
243
- The JavaScript is split into a core library and optional helpers. Use only what you need.
244
-
245
- ### Core (`flash_unified`)
246
-
247
- - `renderFlashMessages()` — scan storages, render to containers, and remove storages.
248
- - `appendMessageToStorage(message, type = 'notice')` — append to the global storage.
249
- - `clearFlashMessages(message?)` — remove rendered messages (all or exact-match only).
250
- - `processMessagePayload(payload)` — accept `{ type, message }[]` or `{ messages: [...] }`.
251
- - `installCustomEventListener()` — subscribe to `flash-unified:messages` and process payloads.
252
- - `storageHasMessages()` — utility to detect existing messages in storage.
253
- - `startMutationObserver()` — (optional / experimental) monitor insertion of storages/templates and render them.
104
+ That's it event handlers that monitor page changes will scan storages and render messages into containers.
254
105
 
255
- Use `appendMessageToStorage()` and `renderFlashMessages()` to produce client-originated Flash messages:
106
+ As mentioned earlier, no changes are required in your controllers for setting Flash messages.
256
107
 
257
- ```js
258
- import { appendMessageToStorage, renderFlashMessages } from "flash_unified";
108
+ **For detailed usage** (customization, API reference, Turbo/network helpers, templates, locales, generator usage, and examples), see [`ADVANCED.md`](ADVANCED.md). Examples for using asset pipelines like Sprockets are also provided.
259
109
 
260
- appendMessageToStorage("File size too large.", "notice");
261
- renderFlashMessages();
262
- ```
263
-
264
- ### Custom event
265
-
266
- To use custom events, run `installCustomEventListener()` during initialization:
267
-
268
- ```js
269
- import { installCustomEventListener } from "flash_unified";
270
- installCustomEventListener();
271
- ```
272
-
273
- Then, at any desired timing, dispatch a `flash-unified:messages` event on the document:
274
-
275
- ```js
276
- // Example: passing an array
277
- document.dispatchEvent(new CustomEvent('flash-unified:messages', {
278
- detail: [
279
- { type: 'notice', message: 'Sent successfully.' },
280
- { type: 'warning', message: 'Expires in one week.' }
281
- ]
282
- }));
283
-
284
- // Example: passing an object
285
- document.dispatchEvent(new CustomEvent('flash-unified:messages', {
286
- detail: { messages: [ { type: 'alert', message: 'Operation was cancelled.' } ] }
287
- }));
288
- ```
289
-
290
- ### Turbo helpers (`flash_unified/turbo_helpers`)
291
-
292
- When using Turbo, partial updates require rendering at the appropriate events. Use the helper to register these listeners:
293
-
294
- - `installTurboRenderListeners()` — register Turbo lifecycle listeners.
295
- - `installTurboIntegration()` — a convenience that combines `installTurboRenderListeners()` and `installCustomEventListener()` (used by `auto.js`).
296
-
297
- ```js
298
- import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
299
- installTurboRenderListeners();
300
- ```
301
-
302
- ### Network/HTTP helpers (`flash_unified/network_helpers`)
303
-
304
- Use these helpers to set messages for network/HTTP errors:
305
-
306
- ```js
307
- import { notifyNetworkError, notifyHttpError } from "flash_unified/network_helpers";
308
-
309
- notifyNetworkError();
310
- notifyHttpError(413);
311
- ```
312
-
313
- The messages used here are output as hidden elements by the server-side view helper `flash_general_error_messages`. The original message strings are installed as I18n translation files in `config/locales` by the generator. To change these messages, edit the translations in the corresponding locale file.
314
-
315
- ### Auto initialization entry (`flash_unified/auto`)
316
-
317
- Importing `flash_unified/auto` runs initialization after DOM ready. The behavior can be controlled with data attributes on `<html>`:
110
+ ## Development
318
111
 
319
- - `data-flash-unified-auto-init="false"` disable automatic initialization.
320
- - `data-flash-unified-enable-network-errors="true"` — also enable network/HTTP error listeners.
112
+ For detailed development and testing procedures, see [DEVELOPMENT.md](DEVELOPMENT.md).
321
113
 
322
- ```erb
323
- <html data-flash-unified-enable-network-errors="true">
324
- ```
325
-
326
- ## Development
114
+ ## Changelog
327
115
 
328
- See [DEVELOPMENT.md](DEVELOPMENT.md) or [DEVELOPMENT.ja.md](DEVELOPMENT.ja.md) for development and testing instructions.
116
+ See the [GitHub Releases page](https://github.com/hiroaki/flash-unified/releases).
329
117
 
330
118
  ## License
331
119
 
332
- This project is licensed under 0BSD (Zero-Clause BSD). See `LICENSE` for details.
120
+ This project is released under the 0BSD (Zero-Clause BSD) license. For details, see [LICENSE](LICENSE).
@@ -27,6 +27,28 @@ module FlashUnified
27
27
  def flash_general_error_messages
28
28
  render partial: "flash_unified/general_error_messages"
29
29
  end
30
+
31
+ # Wrapper helper that renders the common flash-unified pieces in a single
32
+ # call. This is a non-destructive convenience helper which calls the
33
+ # existing partial-rendering helpers in a sensible default order. Pass a
34
+ # hash to disable parts, e.g. `flash_unified_sources(container: false)`.
35
+ def flash_unified_sources(options = {})
36
+ opts = {
37
+ global_storage: true,
38
+ templates: true,
39
+ general_errors: true,
40
+ storage: true,
41
+ container: false
42
+ }.merge(options.transform_keys(&:to_sym))
43
+
44
+ parts = []
45
+ parts << flash_global_storage if opts[:global_storage]
46
+ parts << flash_templates if opts[:templates]
47
+ parts << flash_general_error_messages if opts[:general_errors]
48
+ parts << flash_storage if opts[:storage]
49
+ parts << flash_container if opts[:container]
50
+
51
+ safe_join(parts, "\n")
52
+ end
30
53
  end
31
54
  end
32
-
@@ -0,0 +1 @@
1
+ var d=null;function x(e){if(e!==null&&typeof e!="function")throw new TypeError("Renderer must be a function or null");d=e}function S(e){if(!e||!e.isConnected)return!1;let r=window.getComputedStyle(e);return r&&r.display!=="none"&&r.visibility!=="hidden"&&parseFloat(r.opacity)>0}function N(e={}){let{primaryOnly:r=!1,visibleOnly:n=!1,sortByPriority:t=!1,firstOnly:s=!1,filter:o}=e,i=Array.from(document.querySelectorAll("[data-flash-message-container]"));return r&&(i=i.filter(l=>l.hasAttribute("data-flash-primary")&&l.getAttribute("data-flash-primary")!=="false")),n&&(i=i.filter(S)),typeof o=="function"&&(i=i.filter(o)),t&&i.sort((l,v)=>{let c=Number(l.getAttribute("data-flash-message-container-priority")),m=Number(v.getAttribute("data-flash-message-container-priority")),A=Number.isFinite(c)?c:Number.POSITIVE_INFINITY,C=Number.isFinite(m)?m:Number.POSITIVE_INFINITY;return A-C}),s?i.length>0?[i[0]]:[]:i}function L(){let e=document.documentElement,r=n=>{let t=e.getAttribute(n);if(t!==null)return t===""||t.toLowerCase()==="true"||t==="1"?!0:!(t.toLowerCase()==="false"||t==="0")};return{primaryOnly:r("data-flash-unified-container-primary-only"),visibleOnly:r("data-flash-unified-container-visible-only"),sortByPriority:r("data-flash-unified-container-sort-by-priority"),firstOnly:r("data-flash-unified-container-first-only")}}function F(e){N(L()).forEach(n=>{e.forEach(({type:t,message:s})=>{s&&n.appendChild(I(t,s))})})}function h(){document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){a()},{once:!0}):a()}function a(){let e=g(!1);typeof d=="function"?d(e):F(e)}function g(e=!1){let r=document.querySelectorAll("[data-flash-storage]"),n=[];return r.forEach(t=>{let s=t.querySelector("ul");s&&s.children.length>0&&s.querySelectorAll("li").forEach(o=>{n.push({type:o.dataset.type||"notice",message:o.textContent.trim()})}),e||t.remove()}),n}function O(){return g(!0)}function f(e,r="notice"){let n=document.getElementById("flash-storage");if(!n){console.error('[FlashUnified] #flash-storage not found. Define <div id="flash-storage" style="display:none"></div> in layout.');return}let t=n.querySelector("[data-flash-storage]");t||(t=document.createElement("div"),t.setAttribute("data-flash-storage","true"),t.style.display="none",n.appendChild(t));let s=t.querySelector("ul");s||(s=document.createElement("ul"),t.appendChild(s));let o=document.createElement("li");o.dataset.type=r,o.textContent=e,s.appendChild(o)}function y(){let e=document.documentElement;e.hasAttribute("data-flash-unified-custom-listener")||(e.setAttribute("data-flash-unified-custom-listener","true"),document.addEventListener("flash-unified:messages",function(r){try{M(r.detail)}catch(n){console.error("[FlashUnified] Failed to handle custom payload",n)}}))}function T(e){document.querySelectorAll("[data-flash-message-container]").forEach(r=>{if(typeof e>"u"){r.querySelectorAll("[data-flash-message]")?.forEach(n=>n.remove());return}r.querySelectorAll("[data-flash-message]")?.forEach(n=>{let t=n.querySelector(".flash-message-text");t&&t.textContent.trim()===e&&n.remove()})})}function I(e,r){let n=`flash-message-template-${e}`,t=document.getElementById(n);if(t&&t.content){let s=t.content.firstElementChild;if(!s){console.error(`[FlashUnified] Template #${n} has no root element`);let l=document.createElement("div");return l.setAttribute("role","alert"),l.setAttribute("data-flash-message","true"),l.textContent=r,l}let o=s.cloneNode(!0);o.setAttribute("data-flash-message","true");let i=o.querySelector(".flash-message-text");return i&&(i.textContent=r),o}else{console.error(`[FlashUnified] No template found for type: ${e}`);let s=document.createElement("div");s.setAttribute("role","alert"),s.setAttribute("data-flash-message","true");let o=document.createElement("span");return o.className="flash-message-text",o.textContent=r,s.appendChild(o),s}}function p(){let e=document.querySelectorAll("[data-flash-storage]");for(let r of e){let n=r.querySelector("ul");if(n&&n.children.length>0)return!0}return!1}function M(e){if(!e)return;let r=Array.isArray(e)?e:Array.isArray(e.messages)?e.messages:[];r.length!==0&&(r.forEach(({type:n,message:t})=>{t&&f(String(t),n)}),a())}function R(){let e=document.documentElement;if(e.hasAttribute("data-flash-unified-observer-enabled"))return;e.setAttribute("data-flash-unified-observer-enabled","true"),new MutationObserver(n=>{let t=!1;for(let s of n)s.type==="childList"&&s.addedNodes.forEach(o=>{o instanceof Element&&(o.matches('[data-flash-storage], [data-flash-message-container], template[id^="flash-message-template-"]')&&(t=!0),o.querySelector&&o.querySelector("[data-flash-storage]")&&(t=!0))});t&&a()}).observe(document.body,{childList:!0,subtree:!0})}function u(e){if(p())return;let r=Number(e);if(isNaN(r)||r<0||r>0&&r<400)return;let n;r===0?n="network":n=String(r);let t=document.querySelector("[data-flash-message-container]");if(t&&t.querySelector("[data-flash-message]"))return;let s=document.getElementById("general-error-messages");if(!s){console.error("[FlashUnified] No general error messages element found");return}let o=s.querySelector(`li[data-status="${n}"]`);o?f(o.textContent.trim(),"alert"):console.error(`[FlashUnified] No error message defined for status: ${e}`)}function P(){u(0),a()}function B(e){u(e),a()}function q(){let e=document.documentElement;e.hasAttribute("data-flash-unified-turbo-listeners")||(e.setAttribute("data-flash-unified-turbo-listeners","true"),document.addEventListener("turbo:load",function(){a()}),document.addEventListener("turbo:frame-load",function(){a()}),document.addEventListener("turbo:render",function(){a()}),w())}function w(){let e=new Event("turbo:after-stream-render");document.addEventListener("turbo:before-stream-render",r=>{let n=r.detail.render;r.detail.render=async function(t){await n(t),document.dispatchEvent(e)}}),document.addEventListener("turbo:after-stream-render",function(){a()})}function b(){let e=document.documentElement;e.hasAttribute("data-flash-unified-initialized")||(e.setAttribute("data-flash-unified-initialized","true"),q(),y())}function E(){let e=document.documentElement;e.hasAttribute("data-flash-unified-network-listeners")||(e.setAttribute("data-flash-unified-network-listeners","true"),document.addEventListener("turbo:submit-end",function(r){let n=r.detail.fetchResponse,t;n===void 0?(t=0,console.warn("[FlashUnified] No response received from server. Possible network or proxy error.")):t=n.statusCode,u(t),a()}),document.addEventListener("turbo:fetch-request-error",function(r){u(0),a()}))}if(typeof document<"u"){let e=document.documentElement;if(e.getAttribute("data-flash-unified-auto-init")!=="false"){let n=e.getAttribute("data-flash-unified-enable-network-errors")==="true",t=async()=>{b(),h(),n&&E()};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()}}export{O as aggregateFlashMessages,f as appendMessageToStorage,T as clearFlashMessages,g as consumeFlashMessages,N as getFlashMessageContainers,L as getHtmlContainerOptions,y as installCustomEventListener,h as installInitialRenderListener,E as installNetworkErrorListeners,b as installTurboIntegration,q as installTurboRenderListeners,B as notifyHttpError,P as notifyNetworkError,M as processMessagePayload,a as renderFlashMessages,u as resolveAndAppendErrorMessage,x as setFlashMessageRenderer,R as startMutationObserver,p as storageHasMessages};
@@ -1,66 +1,164 @@
1
- /*
2
- Flash UnifiedMinimal Core API
1
+ /**
2
+ * flash_unified — Core utilities for reading and rendering embedded flash messages.
3
+ *
4
+ * See README.md for full usage examples and integration notes.
5
+ *
6
+ * @module flash_unified
7
+ */
3
8
 
4
- Purpose
5
- - Provide core utilities for flash message rendering.
6
- - Users control when and how to trigger rendering via their own event handlers.
9
+ /**
10
+ * Custom renderer function set by user. When null, defaultRenderer is used.
11
+ * @type {Function|null}
12
+ */
13
+ let customRenderer = null;
7
14
 
8
- Core API:
9
- - renderFlashMessages(): render all messages from storage into containers
10
- - appendMessageToStorage(message, type): add a message to hidden storage
11
- - clearFlashMessages(message?): clear displayed messages
12
- - processMessagePayload(payload): handle message arrays from custom events
13
- - startMutationObserver(): watch for dynamically inserted storage/templates
15
+ /**
16
+ * Set a custom renderer function to replace the default DOM-based rendering.
17
+ * Pass `null` to reset to default behavior.
18
+ *
19
+ * @param {Function|null} fn - A function that receives an array of message objects: [{type, message}, ...]
20
+ * @returns {void}
21
+ * @throws {TypeError} If fn is neither a function nor null
22
+ *
23
+ * @example
24
+ * import { setFlashMessageRenderer } from 'flash_unified';
25
+ * // Use toastr for notifications
26
+ * setFlashMessageRenderer((messages) => {
27
+ * messages.forEach(({ type, message }) => {
28
+ * toastr[type === 'alert' ? 'error' : 'info'](message);
29
+ * });
30
+ * });
31
+ */
32
+ function setFlashMessageRenderer(fn) {
33
+ if (fn !== null && typeof fn !== 'function') {
34
+ throw new TypeError('Renderer must be a function or null');
35
+ }
36
+ customRenderer = fn;
37
+ }
14
38
 
15
- Required DOM (no Rails helpers needed)
16
- 1) Display container (required)
17
- <div data-flash-message-container></div>
39
+ /**
40
+ * Return whether an element is visible (basic heuristic).
41
+ * Considers display/visibility and DOM connection; does not use IntersectionObserver.
42
+ *
43
+ * @param {Element} el
44
+ * @returns {boolean}
45
+ */
46
+ function isVisible(el) {
47
+ if (!el || !el.isConnected) return false;
48
+ const style = window.getComputedStyle(el);
49
+ return style && style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) > 0;
50
+ }
18
51
 
19
- 2) Hidden storage (optional; any number; removed after render)
20
- <div data-flash-storage style="display:none;">
21
- <ul>
22
- <li data-type="notice">Saved</li>
23
- <li data-type="alert">Oops</li>
24
- </ul>
25
- </div>
52
+ /**
53
+ * Collect flash message containers with optional filtering/sorting.
54
+ * By default, returns all elements matching `[data-flash-message-container]`.
55
+ * This function is intended for custom renderers to choose target containers.
56
+ *
57
+ * Options:
58
+ * - primaryOnly?: boolean — If true, only include elements with `data-flash-primary` present or set to "true".
59
+ * - visibleOnly?: boolean — If true, include only elements considered visible.
60
+ * - sortByPriority?: boolean — If true, sort by numeric `data-flash-message-container-priority` ascending (missing treated as Infinity).
61
+ * - firstOnly?: boolean — If true, return at most one element after filtering/sorting.
62
+ * - filter?: (el: Element) => boolean — Additional predicate to include elements.
63
+ *
64
+ * @param {Object} [options]
65
+ * @returns {Element[]} Array of container elements
66
+ */
67
+ function getFlashMessageContainers(options = {}) {
68
+ const {
69
+ primaryOnly = false,
70
+ visibleOnly = false,
71
+ sortByPriority = false,
72
+ firstOnly = false,
73
+ filter
74
+ } = options;
26
75
 
27
- 3) Message templates, one per type (root should have role="alert" and include
28
- a .flash-message-text node for insertion)
29
- <template id="flash-message-template-notice">
30
- <div class="flash-notice" role="alert"><span class="flash-message-text"></span></div>
31
- </template>
32
- <template id="flash-message-template-alert">
33
- <div class="flash-alert" role="alert"><span class="flash-message-text"></span></div>
34
- </template>
76
+ // Fixed selector by gem convention
77
+ let list = Array.from(document.querySelectorAll('[data-flash-message-container]'));
35
78
 
36
- 4) Global storage (required by appendMessageToStorage)
37
- <div id="flash-storage" style="display:none;"></div>
79
+ if (primaryOnly) {
80
+ list = list.filter(el => el.hasAttribute('data-flash-primary') && (el.getAttribute('data-flash-primary') !== 'false'));
81
+ }
82
+ if (visibleOnly) {
83
+ list = list.filter(isVisible);
84
+ }
85
+ if (typeof filter === 'function') {
86
+ list = list.filter(filter);
87
+ }
88
+ if (sortByPriority) {
89
+ list.sort((a, b) => {
90
+ const pa = Number(a.getAttribute('data-flash-message-container-priority'));
91
+ const pb = Number(b.getAttribute('data-flash-message-container-priority'));
92
+ const va = Number.isFinite(pa) ? pa : Number.POSITIVE_INFINITY;
93
+ const vb = Number.isFinite(pb) ? pb : Number.POSITIVE_INFINITY;
94
+ return va - vb;
95
+ });
96
+ }
97
+ if (firstOnly) {
98
+ return list.length > 0 ? [list[0]] : [];
99
+ }
100
+ return list;
101
+ }
38
102
 
39
- Usage Examples:
40
- // Manual control with Stimulus
41
- import { renderFlashMessages, appendMessageToStorage } from "flash_unified";
42
- export default class extends Controller {
43
- connect() { renderFlashMessages(); }
44
- error() {
45
- appendMessageToStorage('Error occurred', 'alert');
46
- renderFlashMessages();
47
- }
48
- }
103
+ /**
104
+ * Read default container selection options from <html> data-attributes.
105
+ * Supported attributes (all optional):
106
+ * - data-flash-unified-container-primary-only
107
+ * - data-flash-unified-container-visible-only
108
+ * - data-flash-unified-container-sort-by-priority
109
+ * - data-flash-unified-container-first-only
110
+ *
111
+ * Each attribute accepts:
112
+ * - presence with no value → true
113
+ * - "true"/"1" → true
114
+ * - "false"/"0" → false
115
+ * Missing attribute yields undefined (does not override defaults).
116
+ *
117
+ * @returns {{ primaryOnly?: boolean, visibleOnly?: boolean, sortByPriority?: boolean, firstOnly?: boolean }}
118
+ */
119
+ function getHtmlContainerOptions() {
120
+ const root = document.documentElement;
121
+ const parse = (name) => {
122
+ const val = root.getAttribute(name);
123
+ if (val === null) return undefined;
124
+ if (val === '' || val.toLowerCase() === 'true' || val === '1') return true;
125
+ if (val.toLowerCase() === 'false' || val === '0') return false;
126
+ // Any other non-empty value: treat as true for convenience
127
+ return true;
128
+ };
129
+ return {
130
+ primaryOnly: parse('data-flash-unified-container-primary-only'),
131
+ visibleOnly: parse('data-flash-unified-container-visible-only'),
132
+ sortByPriority: parse('data-flash-unified-container-sort-by-priority'),
133
+ firstOnly: parse('data-flash-unified-container-first-only')
134
+ };
135
+ }
49
136
 
50
- // Custom event listener
51
- import { renderFlashMessages } from "flash_unified";
52
- document.addEventListener('turbo:load', renderFlashMessages);
53
- document.addEventListener('my-app:show-message', (event) => {
54
- appendMessageToStorage(event.detail.message, event.detail.type);
55
- renderFlashMessages();
137
+ /**
138
+ * Default renderer: renders messages into DOM containers using templates.
139
+ *
140
+ * @param {{type: string, message: string}[]} messages - Array of message objects
141
+ * @returns {void}
142
+ */
143
+ function defaultRenderer(messages) {
144
+ // Allow page-wide defaults via <html> data-attributes
145
+ const containers = getFlashMessageContainers(getHtmlContainerOptions());
146
+ containers.forEach(container => {
147
+ messages.forEach(({ type, message }) => {
148
+ if (message) container.appendChild(createFlashMessageNode(type, message));
56
149
  });
57
- */
150
+ });
151
+ }
58
152
 
59
- /* 初回描画リスナーをセットします。
60
- DOMContentLoaded 時に renderFlashMessages() を一度だけ呼びます。
61
- ---
62
- Install a listener to render flash messages on DOMContentLoaded (once).
63
- */
153
+ /**
154
+ * Install a one-time listener that calls `renderFlashMessages()` on initial page load.
155
+ *
156
+ * @example
157
+ * import { installInitialRenderListener } from 'flash_unified';
158
+ * installInitialRenderListener();
159
+ *
160
+ * @returns {void}
161
+ */
64
162
  function installInitialRenderListener() {
65
163
  if (document.readyState === 'loading') {
66
164
  document.addEventListener('DOMContentLoaded', function() { renderFlashMessages(); }, { once: true });
@@ -69,19 +167,39 @@ function installInitialRenderListener() {
69
167
  }
70
168
  }
71
169
 
72
- /* ストレージにあるメッセージを表示させます。
73
- すべての [data-flash-storage] 内のリスト項目を集約し、各項目ごとにテンプレートを用いて
74
- フラッシュメッセージ要素を生成し、[data-flash-message-container] に追加します。
75
- 処理後は各ストレージ要素を取り除きます。
76
- ---
77
- Render messages found in all [data-flash-storage] lists, create elements via templates,
78
- and append them into [data-flash-message-container]. Each storage is removed after processing.
79
- */
170
+ /**
171
+ * Render messages found in storages into message containers.
172
+ * Delegates message collection to `consumeFlashMessages(false)`, which removes the storage elements.
173
+ * Uses custom renderer if set, otherwise uses default DOM-based rendering.
174
+ *
175
+ * @example
176
+ * import { renderFlashMessages } from 'flash_unified';
177
+ * renderFlashMessages();
178
+ *
179
+ * @returns {void}
180
+ */
80
181
  function renderFlashMessages() {
81
- const storages = document.querySelectorAll('[data-flash-storage]');
82
- const containers = document.querySelectorAll('[data-flash-message-container]');
182
+ const messages = consumeFlashMessages(false);
183
+
184
+ if (typeof customRenderer === 'function') {
185
+ customRenderer(messages);
186
+ } else {
187
+ defaultRenderer(messages);
188
+ }
189
+ }
83
190
 
84
- // Aggregated messages list
191
+ /**
192
+ * Collect messages from all `[data-flash-storage]` elements.
193
+ * By default, removes each storage after reading; pass `keep = true` to preserve them.
194
+ *
195
+ * @param {boolean} [keep=false] - When true, do not remove storage elements after reading.
196
+ * @returns {{type: string, message: string}[]} Array of message objects.
197
+ *
198
+ * @example
199
+ * const msgs = consumeFlashMessages(true);
200
+ */
201
+ function consumeFlashMessages(keep = false) {
202
+ const storages = document.querySelectorAll('[data-flash-storage]');
85
203
  const messages = [];
86
204
  storages.forEach(storage => {
87
205
  const ul = storage.querySelector('ul');
@@ -90,33 +208,38 @@ function renderFlashMessages() {
90
208
  messages.push({ type: li.dataset.type || 'notice', message: li.textContent.trim() });
91
209
  });
92
210
  }
93
- // Remove storage after consuming
94
- storage.remove();
211
+ if (!keep) storage.remove();
95
212
  });
213
+ return messages;
214
+ }
96
215
 
97
- containers.forEach(container => {
98
- messages.forEach(({ type, message }) => {
99
- if (message) container.appendChild(createFlashMessageNode(type, message));
100
- });
101
- });
216
+ /**
217
+ * Return messages without removing the storage elements.
218
+ * Thin wrapper over `consumeFlashMessages(true)`.
219
+ *
220
+ * @returns {{type: string, message: string}[]}
221
+ *
222
+ * @example
223
+ * const msgs = aggregateFlashMessages();
224
+ */
225
+ function aggregateFlashMessages() {
226
+ return consumeFlashMessages(true);
102
227
  }
103
228
 
104
- /* フラッシュ・メッセージ項目として message をデータとして埋め込みます。
105
- 埋め込まれた項目は renderFlashMessages を呼び出すことによって表示されます。
106
- ---
107
- Append a message item into the hidden storage.
108
- Call renderFlashMessages() to display it.
109
- */
229
+ /**
230
+ * Append a message to the global storage element (`#flash-storage`).
231
+ *
232
+ * @param {string} message - The message text to append.
233
+ * @param {string} [type='notice'] - The flash type (e.g. 'notice', 'alert').
234
+ * @returns {void}
235
+ *
236
+ * @example
237
+ * appendMessageToStorage('Saved', 'notice');
238
+ */
110
239
  function appendMessageToStorage(message, type = 'notice') {
111
240
  const storageContainer = document.getElementById("flash-storage");
112
241
  if (!storageContainer) {
113
242
  console.error('[FlashUnified] #flash-storage not found. Define <div id="flash-storage" style="display:none"></div> in layout.');
114
- // TODO: あるいは自動生成して document.body.appendChild しますか?
115
- // ユーザの目に見えない部分で要素が増えることを避けたいと考え、警告に留めています。
116
- // 下で storage を生成する部分は、ユーザが設定するコンテナの中なので問題ありません。
117
- // ---
118
- // Alternatively we could auto-create it on document.body, but we avoid hidden side-effects.
119
- // Creating the inner [data-flash-storage] below is safe since it's inside the user-provided container.
120
243
  return;
121
244
  }
122
245
 
@@ -140,12 +263,17 @@ function appendMessageToStorage(message, type = 'notice') {
140
263
  ul.appendChild(li);
141
264
  }
142
265
 
143
- /* カスタムイベントリスナーを設定します(オプション)。
144
- サーバーや他のJSからのカスタムイベントを受け取ります。
145
- ---
146
- Setup custom event listener for programmatic message dispatch.
147
- Listen for "flash-unified:messages" events from server or other JS.
148
- */
266
+ /**
267
+ * Install a listener for `flash-unified:messages` CustomEvent and process its payload.
268
+ * The event's `detail` should be either an array of message objects or an object with a `messages` array.
269
+ *
270
+ * @example
271
+ * document.dispatchEvent(new CustomEvent('flash-unified:messages', {
272
+ * detail: [{ type: 'notice', message: 'Hi' }]
273
+ * }));
274
+ *
275
+ * @returns {void}
276
+ */
149
277
  function installCustomEventListener() {
150
278
  const root = document.documentElement;
151
279
  if (root.hasAttribute('data-flash-unified-custom-listener')) return; // idempotent
@@ -160,22 +288,20 @@ function installCustomEventListener() {
160
288
  });
161
289
  }
162
290
 
163
- /* フラッシュ・メッセージの表示をクリアします。
164
- message が指定されている場合は、そのメッセージを含んだフラッシュ・メッセージのみを削除します。
165
- 省略された場合はすべてのフラッシュ・メッセージが対象です。
166
- ---
167
- Clear flash messages. If message is provided, remove only matching ones;
168
- otherwise remove all flash message nodes in the containers.
169
- */
291
+ /**
292
+ * Clear rendered flash messages from message containers.
293
+ * If `message` is provided, only remove elements whose text exactly matches it.
294
+ *
295
+ * @param {string} [message] - Exact message text to remove (optional).
296
+ * @returns {void}
297
+ */
170
298
  function clearFlashMessages(message) {
171
299
  document.querySelectorAll('[data-flash-message-container]').forEach(container => {
172
- // メッセージ指定なし: メッセージ要素のみ全削除(コンテナ内の他要素は残す)
173
300
  if (typeof message === 'undefined') {
174
301
  container.querySelectorAll('[data-flash-message]')?.forEach(n => n.remove());
175
302
  return;
176
303
  }
177
304
 
178
- // 指定メッセージに一致する要素だけ削除
179
305
  container.querySelectorAll('[data-flash-message]')?.forEach(n => {
180
306
  const text = n.querySelector('.flash-message-text');
181
307
  if (text && text.textContent.trim() === message) n.remove();
@@ -183,15 +309,14 @@ function clearFlashMessages(message) {
183
309
  });
184
310
  }
185
311
 
186
- // --- ユーティリティ関数 / Utility functions ---
187
-
188
- /* テンプレートからフラッシュ・メッセージ要素を生成します。
189
- type に対応する <template id="flash-message-template-<type>"> を利用し、
190
- .flash-message-text に文言を挿入します。テンプレートが無い場合は簡易的な要素を生成します。
191
- ---
192
- Create a flash message DOM node using <template id="flash-message-template-<type>">.
193
- Inserts the message into .flash-message-text. Falls back to a minimal element when template is missing.
194
- */
312
+ /**
313
+ * Create a DOM node for a flash message using the `flash-message-template-<type>` template.
314
+ * Falls back to a minimal element when the template is missing.
315
+ *
316
+ * @param {string} type
317
+ * @param {string} message
318
+ * @returns {Element}
319
+ */
195
320
  function createFlashMessageNode(type, message) {
196
321
  const templateId = `flash-message-template-${type}`;
197
322
  const template = document.getElementById(templateId);
@@ -212,7 +337,7 @@ function createFlashMessageNode(type, message) {
212
337
  return root;
213
338
  } else {
214
339
  console.error(`[FlashUnified] No template found for type: ${type}`);
215
- // テンプレートがない場合は生成 / Fallback element when template is missing
340
+ // Fallback element when template is missing
216
341
  const node = document.createElement('div');
217
342
  node.setAttribute('role', 'alert');
218
343
  node.setAttribute('data-flash-message', 'true');
@@ -224,10 +349,11 @@ function createFlashMessageNode(type, message) {
224
349
  }
225
350
  }
226
351
 
227
- /* 何らかのストレージにメッセージが存在するかを判定します。
228
- ---
229
- Return true if any [data-flash-storage] contains at least one <li> item.
230
- */
352
+ /**
353
+ * Return true if any `[data-flash-storage]` contains at least one `<li>`.
354
+ *
355
+ * @returns {boolean}
356
+ */
231
357
  function storageHasMessages() {
232
358
  const storages = document.querySelectorAll('[data-flash-storage]');
233
359
  for (const storage of storages) {
@@ -239,11 +365,15 @@ function storageHasMessages() {
239
365
  return false;
240
366
  }
241
367
 
242
- /* メッセージの配列(または { messages: [...] })を受け取り、ストレージに追加して描画します。
243
- ---
244
- Handle a payload of messages and render them.
245
- Accepts either an array of { type, message } or an object { messages: [...] }.
246
- */
368
+ /**
369
+ * Accept either:
370
+ * - an array of message objects [{ type, message }, ...], or
371
+ * - an object { messages: [...] } where messages is such an array.
372
+ * Append each message to storage and trigger rendering.
373
+ *
374
+ * @param {Array|Object} payload
375
+ * @returns {void}
376
+ */
247
377
  function processMessagePayload(payload) {
248
378
  if (!payload) return;
249
379
  const list = Array.isArray(payload)
@@ -257,13 +387,12 @@ function processMessagePayload(payload) {
257
387
  renderFlashMessages();
258
388
  }
259
389
 
260
- /* 任意: MutationObserver を有効化し、動的に挿入されたストレージ/テンプレートを検出して描画します。
261
- サーバーレスポンス側でカスタムイベントを発火できない場合の代替となります。
262
- ---
263
- Optional: Enable a MutationObserver that watches for dynamically inserted
264
- flash storage or templates and triggers rendering. Useful when you cannot
265
- or do not want to dispatch a custom event from server responses.
266
- */
390
+ /**
391
+ * Enable a MutationObserver that watches for dynamically inserted storages, templates,
392
+ * or message containers and triggers rendering. Useful when server responses cannot dispatch events.
393
+ *
394
+ * @returns {void}
395
+ */
267
396
  function startMutationObserver() {
268
397
  const root = document.documentElement;
269
398
  if (root.hasAttribute('data-flash-unified-observer-enabled')) return;
@@ -297,11 +426,16 @@ function startMutationObserver() {
297
426
 
298
427
  export {
299
428
  renderFlashMessages,
429
+ setFlashMessageRenderer,
430
+ getFlashMessageContainers,
431
+ getHtmlContainerOptions,
300
432
  appendMessageToStorage,
301
433
  clearFlashMessages,
302
434
  processMessagePayload,
303
435
  startMutationObserver,
304
436
  installCustomEventListener,
305
437
  installInitialRenderListener,
306
- storageHasMessages
438
+ storageHasMessages,
439
+ consumeFlashMessages,
440
+ aggregateFlashMessages
307
441
  };
@@ -35,7 +35,9 @@ Gem::Specification.new do |spec|
35
35
  "README.md",
36
36
  "CHANGELOG.md",
37
37
  "flash_unified.gemspec"
38
- ].reject { |f| File.directory?(f) }
38
+ ].reject do |f|
39
+ File.directory?(f) || f == "app/javascript/flash_unified/all.entry.js"
40
+ end
39
41
  # Ensure version file is included
40
42
  files << "lib/flash_unified/version.rb" unless files.include?("lib/flash_unified/version.rb")
41
43
  files.uniq
@@ -27,6 +27,7 @@ module FlashUnified
27
27
  flash_unified/auto.js
28
28
  flash_unified/turbo_helpers.js
29
29
  flash_unified/network_helpers.js
30
+ flash_unified/all.bundle.js
30
31
  ]
31
32
  else
32
33
  # Fallback: still add to assets paths if available
@@ -4,45 +4,25 @@ require "rails/generators/base"
4
4
  module FlashUnified
5
5
  module Generators
6
6
  class InstallGenerator < Rails::Generators::Base
7
- desc "Copies FlashUnified javascript, view partials, locales and prints setup instructions (Importmap / asset pipeline)."
7
+ desc <<~DESC
8
+ Copies FlashUnified javascript, view partials, locales and prints setup instructions (Importmap / asset pipeline).
8
9
 
9
- class_option :force, type: :boolean, default: false, desc: "Overwrite existing files"
10
-
11
- # Print a clear start message so users see the generator run boundary.
12
- # Using `say_status :run` follows the Rails generator convention (colored label).
13
- # Print a start message once per generator run. An optional `note` will be
14
- # appended to the message to provide context (e.g. "copy javascript").
15
- def start_message(note = nil)
16
- return if @flash_unified_started
17
- message = "Installing FlashUnified"
18
- message += " — #{note}" if note
19
- say_status :run, message, :blue
20
- @flash_unified_started = true
21
- end
22
-
23
- def copy_javascript
24
- start_message("copy javascript")
25
- installer = FlashUnified::Installer.new(source_root: File.expand_path('../../../../', __dir__), target_root: Dir.pwd, force: options[:force])
26
- installer.copy_javascript do |status, path|
27
- say_status status, display_path(path)
28
- end
29
- end
10
+ By default (no options), only templates and locales are copied.
11
+ Use --all to copy all groups (javascript, templates, views, locales, helpers).
12
+ Use --templates or --locales for fine-grained control.
13
+ DESC
30
14
 
31
- # View partials are copied into your host app so you can customize them.
32
- def copy_view_partials
33
- start_message("copy view partials")
34
- installer = FlashUnified::Installer.new(source_root: File.expand_path('../../../../', __dir__), target_root: Dir.pwd, force: options[:force])
35
- installer.copy_views do |status, path|
36
- say_status status, display_path(path)
37
- end
38
- end
39
-
40
- def copy_locales
41
- start_message("copy locales")
42
- installer = FlashUnified::Installer.new(source_root: File.expand_path('../../../../', __dir__), target_root: Dir.pwd, force: options[:force])
43
- installer.copy_locales do |status, path|
44
- say_status status, display_path(path)
45
- end
15
+ class_option :force, type: :boolean, default: false, desc: "Overwrite existing files"
16
+ class_option :all, type: :boolean, default: false, desc: "Install all files"
17
+ class_option :templates, type: :boolean, default: false, desc: "Install only _templates.html.erb partial"
18
+ class_option :views, type: :boolean, default: false, desc: "Install all view partials (views/flash_unified/*)"
19
+ class_option :javascript, type: :boolean, default: false, desc: "Install only JavaScript files"
20
+ class_option :locales, type: :boolean, default: false, desc: "Install only locale files"
21
+ class_option :helpers, type: :boolean, default: false, desc: "Install only helper files"
22
+
23
+ # Rails generator entrypoint (only task that performs copying)
24
+ def install
25
+ handle_installation
46
26
  end
47
27
 
48
28
  def show_importmap_instructions
@@ -50,76 +30,63 @@ module FlashUnified
50
30
 
51
31
  === FlashUnified installation instructions ===
52
32
 
53
- Importing the JavaScript
54
- - Importmap: add to `config/importmap.rb`:
33
+ Quick start (Importmap)
34
+ 1) Add to `config/importmap.rb`:
35
+
36
+ pin "flash_unified/all", to: "flash_unified/all.bundle.js"
37
+
38
+ 2) Import once in your JavaScript entry point (e.g. `app/javascript/application.js`):
55
39
 
56
- pin "flash_unified", to: "flash_unified/flash_unified.js"
57
- pin "flash_unified/auto", to: "flash_unified/auto.js"
58
- pin "flash_unified/turbo_helpers", to: "flash_unified/turbo_helpers.js"
59
- pin "flash_unified/network_helpers", to: "flash_unified/network_helpers.js"
40
+ import "flash_unified/all";
60
41
 
61
- Quick start (auto init):
42
+ 3) In your layout (inside `<body>`):
62
43
 
63
- Importing `flash_unified/auto` sets up Turbo listeners and triggers an initial render.
44
+ <%= flash_unified_sources %>
45
+ <%= flash_container %>
64
46
 
65
- import "flash_unified/auto";
66
- // Configure via <html data-flash-unified-*>:
67
- // data-flash-unified-auto-init="false" (opt-out)
68
- // data-flash-unified-enable-network-errors="true" (install Turbo network error listeners)
47
+ Auto-initialization is enabled by default. Control it via `<html>` attributes:
48
+ - `data-flash-unified-auto-init="false"` to disable all automatic wiring.
49
+ - `data-flash-unified-enable-network-errors="true"` to enable network error listeners.
69
50
 
51
+ Advanced usage (optional)
70
52
  Manual control:
71
53
 
72
54
  import { renderFlashMessages, appendMessageToStorage } from "flash_unified";
73
55
  import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
74
56
  installTurboRenderListeners();
75
57
 
76
- Network helpers (optional, framework-agnostic):
58
+ Network helpers:
77
59
 
78
60
  import { notifyNetworkError, notifyHttpError } from "flash_unified/network_helpers";
79
61
  // notifyNetworkError();
80
62
  // notifyHttpError(413);
81
63
 
82
- - Asset pipeline (Propshaft / Sprockets): the engine adds its `app/javascript` to the asset paths; add modulepreload links and an inline importmap in your layout's `<head>` and import the bare specifier.
64
+ Propshaft / Sprockets quick start
65
+ Place in `<head>`:
83
66
 
84
- <link rel="modulepreload" href="<%= asset_path('flash_unified/flash_unified.js') %>">
85
- <link rel="modulepreload" href="<%= asset_path('flash_unified/network_helpers.js') %>">
86
- <link rel="modulepreload" href="<%= asset_path('flash_unified/turbo_helpers.js') %>">
87
- <link rel="modulepreload" href="<%= asset_path('flash_unified/auto.js') %>">
67
+ <link rel="modulepreload" href="<%= asset_path('flash_unified/all.bundle.js') %>">
88
68
  <script type="importmap">
89
69
  {
90
70
  "imports": {
91
- "flash_unified": "<%= asset_path('flash_unified/flash_unified.js') %>",
92
- "flash_unified/auto": "<%= asset_path('flash_unified/auto.js') %>",
93
- "flash_unified/turbo_helpers": "<%= asset_path('flash_unified/turbo_helpers.js') %>",
94
- "flash_unified/network_helpers": "<%= asset_path('flash_unified/network_helpers.js') %>"
71
+ "flash_unified/all": "<%= asset_path('flash_unified/all.bundle.js') %>"
95
72
  }
96
73
  }
97
74
  </script>
98
75
  <script type="module">
99
- import "flash_unified/auto";
76
+ import "flash_unified/all";
100
77
  </script>
101
78
 
102
- Remove the `import "flash_unified/auto";` line if you don't want automatic initialization.
103
-
104
- How to place partials in your layout
105
- - The gem's view helpers render engine partials. After running this generator you'll have the partials available under `app/views/flash_unified` and can customize them as needed.
106
-
107
- Recommended layout snippet (inside `<body>`, global helpers):
79
+ (Optionally map `flash_unified` or other modules if you need manual control APIs.)
108
80
 
81
+ Layout helpers
109
82
  <%= flash_general_error_messages %>
110
83
  <%= flash_global_storage %>
111
84
  <%= flash_templates %>
112
-
113
- Place the visible container wherever messages should appear:
114
-
115
85
  <%= flash_container %>
116
-
117
- Embed per-response storage inside content (e.g. Turbo Frame responses):
118
-
119
86
  <%= flash_storage %>
120
87
 
121
88
  Documentation
122
- - For full details and customization guidance, see README.md / README.ja.md in the gem.
89
+ - See README.md / README.ja.md for customization guidance and advanced scenarios.
123
90
 
124
91
  MSG
125
92
 
@@ -128,6 +95,96 @@ module FlashUnified
128
95
 
129
96
  private
130
97
 
98
+ no_tasks do
99
+ # Print a clear start message so users see the generator run boundary.
100
+ # Using `say_status :run` follows the Rails generator convention (colored label).
101
+ # Print a start message once per generator run. An optional `note` will be
102
+ # appended to the message to provide context (e.g. "copy javascript").
103
+ def start_message(note = nil)
104
+ return if @flash_unified_started
105
+ message = "Installing FlashUnified"
106
+ message += " — #{note}" if note
107
+ say_status :run, message, :blue
108
+ @flash_unified_started = true
109
+ end
110
+
111
+ # Resolve gem root robustly by walking up until we find the gemspec
112
+ def gem_root
113
+ return @gem_root if defined?(@gem_root)
114
+ path = Pathname.new(__dir__)
115
+ path.ascend do |p|
116
+ if (p + 'flash_unified.gemspec').exist?
117
+ @gem_root = p
118
+ break
119
+ end
120
+ end
121
+ @gem_root ||= Pathname.new(File.expand_path('../../../../', __dir__))
122
+ end
123
+
124
+ def installer
125
+ @installer ||= FlashUnified::Installer.new(source_root: gem_root.to_s, target_root: destination_root, force: options[:force])
126
+ end
127
+
128
+ def handle_installation
129
+ # Determine which groups to install
130
+ groups = []
131
+ groups << :javascript if options[:javascript]
132
+ groups << :templates if options[:templates]
133
+ groups << :views if options[:views]
134
+ groups << :locales if options[:locales]
135
+ groups << :helpers if options[:helpers]
136
+ # If --all was provided, install every group. Otherwise, when no
137
+ # explicit groups are requested, install a sensible minimal set:
138
+ # templates + locales. This avoids unexpectedly copying JavaScript,
139
+ # helpers or full view partial sets into the host app when the user
140
+ # runs the generator without options.
141
+ if options[:all]
142
+ groups = [:javascript, :templates, :views, :locales, :helpers]
143
+ elsif groups.empty?
144
+ groups = [:templates, :locales]
145
+ end
146
+
147
+ groups.each do |group|
148
+ send("copy_#{group}")
149
+ end
150
+ end
151
+
152
+ def copy_javascript
153
+ start_message("copy javascript")
154
+ installer.copy_javascript do |status, path|
155
+ say_status status, display_path(path)
156
+ end
157
+ end
158
+
159
+ def copy_templates
160
+ start_message("copy _templates.html.erb")
161
+ installer.copy_templates do |status, path|
162
+ say_status status, display_path(path)
163
+ end
164
+ end
165
+
166
+ def copy_views
167
+ start_message("copy all view partials")
168
+ installer.copy_views do |status, path|
169
+ say_status status, display_path(path)
170
+ end
171
+ end
172
+
173
+ def copy_locales
174
+ start_message("copy locales")
175
+ installer.copy_locales do |status, path|
176
+ say_status status, display_path(path)
177
+ end
178
+ end
179
+
180
+ def copy_helpers
181
+ start_message("copy helpers")
182
+ installer.copy_helpers do |status, path|
183
+ say_status status, display_path(path)
184
+ end
185
+ end
186
+ end
187
+
131
188
  # Return a user-friendly path for display in generator output. If the
132
189
  # provided path is under the current working directory (Rails root), show
133
190
  # it as a relative path; otherwise show the original path.
@@ -8,8 +8,9 @@ module FlashUnified
8
8
  class Installer
9
9
  attr_reader :source_root, :target_root, :force
10
10
 
11
- def initialize(source_root:, target_root:, force: false)
12
- @source_root = Pathname.new(source_root)
11
+ def initialize(source_root: nil, target_root:, force: false)
12
+ # When source_root is not given, assume the gem root two levels up from this file (lib/flash_unified)
13
+ @source_root = Pathname.new(source_root || File.expand_path('../..', __dir__))
13
14
  @target_root = Pathname.new(target_root)
14
15
  @force = !!force
15
16
  end
@@ -20,16 +21,20 @@ module FlashUnified
20
21
  copy_tree(src, dst, &block)
21
22
  end
22
23
 
24
+ # Copy only _templates.html.erb
25
+ def copy_templates(&block)
26
+ src_dir = source_root.join('app', 'views', 'flash_unified')
27
+ dst_dir = target_root.join('app', 'views', 'flash_unified')
28
+ files = %w[_templates.html.erb]
29
+ copy_files(files, src_dir, dst_dir, &block)
30
+ end
31
+
32
+ # Copy all files in views/flash_unified/ directory
23
33
  def copy_views(&block)
24
34
  src_dir = source_root.join('app', 'views', 'flash_unified')
25
35
  dst_dir = target_root.join('app', 'views', 'flash_unified')
26
- files = %w[
27
- _templates.html.erb
28
- _storage.html.erb
29
- _global_storage.html.erb
30
- _container.html.erb
31
- _general_error_messages.html.erb
32
- ]
36
+ raise "source missing: #{src_dir}" unless src_dir.directory?
37
+ files = Dir.children(src_dir).select { |f| File.file?(src_dir.join(f)) }
33
38
  copy_files(files, src_dir, dst_dir, &block)
34
39
  end
35
40
 
@@ -41,6 +46,15 @@ module FlashUnified
41
46
  copy_files(files, src_dir, dst_dir, &block)
42
47
  end
43
48
 
49
+ def copy_helpers(&block)
50
+ src_dir = source_root.join('app', 'helpers', 'flash_unified')
51
+ dst_dir = target_root.join('app', 'helpers', 'flash_unified')
52
+ files = %w[
53
+ view_helper.rb
54
+ ]
55
+ copy_files(files, src_dir, dst_dir, &block)
56
+ end
57
+
44
58
  private
45
59
 
46
60
  def copy_tree(src, dst, &block)
@@ -1,3 +1,3 @@
1
1
  module FlashUnified
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flash_unified
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hiroaki
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-13 00:00:00.000000000 Z
11
+ date: 2025-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -161,6 +161,7 @@ files:
161
161
  - LICENSE
162
162
  - README.md
163
163
  - app/helpers/flash_unified/view_helper.rb
164
+ - app/javascript/flash_unified/all.bundle.js
164
165
  - app/javascript/flash_unified/auto.js
165
166
  - app/javascript/flash_unified/flash_unified.js
166
167
  - app/javascript/flash_unified/network_helpers.js