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 +4 -4
- data/README.md +44 -256
- data/app/helpers/flash_unified/view_helper.rb +23 -1
- data/app/javascript/flash_unified/all.bundle.js +1 -0
- data/app/javascript/flash_unified/flash_unified.js +259 -125
- data/flash_unified.gemspec +3 -1
- data/lib/flash_unified/engine.rb +1 -0
- data/lib/flash_unified/generators/install/install_generator.rb +130 -73
- data/lib/flash_unified/installer.rb +23 -9
- data/lib/flash_unified/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ff9e758b822e2e06c38f1543fc5e2be3b4aa101879bdd1137ab70f01b0cc350
|
|
4
|
+
data.tar.gz: 1df992f44cd7c2db0992b4130d9047af5c7017a26e3367aa395597a884adbddb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
-
|
|
13
|
+
We had two challenges at the same time.
|
|
14
14
|
|
|
15
|
-
One
|
|
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
|
|
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
|
|
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
|
|
24
|
-
2.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
66
|
+
Run the command:
|
|
91
67
|
```bash
|
|
92
68
|
bundle install
|
|
93
69
|
```
|
|
94
70
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
### 1. Place files
|
|
98
|
-
|
|
99
|
-
Run the installer generator:
|
|
100
|
-
|
|
71
|
+
Or install it directly:
|
|
101
72
|
```bash
|
|
102
|
-
|
|
73
|
+
gem install flash_unified
|
|
103
74
|
```
|
|
104
75
|
|
|
105
|
-
### 2.
|
|
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/
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
88
|
+
### 3. Server-side setup
|
|
215
89
|
|
|
90
|
+
Place the "sources" with the helper right after `<body>` in your layout:
|
|
216
91
|
```erb
|
|
217
|
-
|
|
92
|
+
<body>
|
|
93
|
+
<%= flash_unified_sources %>
|
|
94
|
+
...
|
|
218
95
|
```
|
|
219
96
|
|
|
220
|
-
|
|
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
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
As mentioned earlier, no changes are required in your controllers for setting Flash messages.
|
|
256
107
|
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
<html data-flash-unified-enable-network-errors="true">
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
## Development
|
|
114
|
+
## Changelog
|
|
327
115
|
|
|
328
|
-
See [
|
|
116
|
+
See the [GitHub Releases page](https://github.com/hiroaki/flash-unified/releases).
|
|
329
117
|
|
|
330
118
|
## License
|
|
331
119
|
|
|
332
|
-
This project is
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
82
|
-
|
|
182
|
+
const messages = consumeFlashMessages(false);
|
|
183
|
+
|
|
184
|
+
if (typeof customRenderer === 'function') {
|
|
185
|
+
customRenderer(messages);
|
|
186
|
+
} else {
|
|
187
|
+
defaultRenderer(messages);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
83
190
|
|
|
84
|
-
|
|
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
|
-
|
|
94
|
-
storage.remove();
|
|
211
|
+
if (!keep) storage.remove();
|
|
95
212
|
});
|
|
213
|
+
return messages;
|
|
214
|
+
}
|
|
96
215
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
};
|
data/flash_unified.gemspec
CHANGED
|
@@ -35,7 +35,9 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
"README.md",
|
|
36
36
|
"CHANGELOG.md",
|
|
37
37
|
"flash_unified.gemspec"
|
|
38
|
-
].reject
|
|
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
|
data/lib/flash_unified/engine.rb
CHANGED
|
@@ -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
|
|
7
|
+
desc <<~DESC
|
|
8
|
+
Copies FlashUnified javascript, view partials, locales and prints setup instructions (Importmap / asset pipeline).
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def
|
|
41
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
3) In your layout (inside `<body>`):
|
|
62
43
|
|
|
63
|
-
|
|
44
|
+
<%= flash_unified_sources %>
|
|
45
|
+
<%= flash_container %>
|
|
64
46
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
64
|
+
Propshaft / Sprockets quick start
|
|
65
|
+
Place in `<head>`:
|
|
83
66
|
|
|
84
|
-
<link rel="modulepreload" href="<%= asset_path('flash_unified/
|
|
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/
|
|
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/
|
|
76
|
+
import "flash_unified/all";
|
|
100
77
|
</script>
|
|
101
78
|
|
|
102
|
-
|
|
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
|
-
-
|
|
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
|
|
12
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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)
|
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.
|
|
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-
|
|
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
|