flash_unified 0.1.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 +7 -0
- data/LICENSE +5 -0
- data/README.md +332 -0
- data/app/helpers/flash_unified/view_helper.rb +32 -0
- data/app/javascript/flash_unified/auto.js +54 -0
- data/app/javascript/flash_unified/flash_unified.js +307 -0
- data/app/javascript/flash_unified/network_helpers.js +83 -0
- data/app/javascript/flash_unified/turbo_helpers.js +135 -0
- data/app/views/flash_unified/_container.html.erb +1 -0
- data/app/views/flash_unified/_general_error_messages.html.erb +12 -0
- data/app/views/flash_unified/_global_storage.html.erb +1 -0
- data/app/views/flash_unified/_storage.html.erb +7 -0
- data/app/views/flash_unified/_templates.html.erb +16 -0
- data/config/locales/http_status_messages.en.yml +43 -0
- data/config/locales/http_status_messages.ja.yml +43 -0
- data/flash_unified.gemspec +61 -0
- data/lib/flash_unified/engine.rb +37 -0
- data/lib/flash_unified/generators/install/install_generator.rb +147 -0
- data/lib/flash_unified/installer.rb +95 -0
- data/lib/flash_unified/railtie.rb +13 -0
- data/lib/flash_unified/version.rb +3 -0
- data/lib/flash_unified.rb +19 -0
- metadata +209 -0
@@ -0,0 +1,307 @@
|
|
1
|
+
/*
|
2
|
+
Flash Unified — Minimal Core API
|
3
|
+
|
4
|
+
Purpose
|
5
|
+
- Provide core utilities for flash message rendering.
|
6
|
+
- Users control when and how to trigger rendering via their own event handlers.
|
7
|
+
|
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
|
14
|
+
|
15
|
+
Required DOM (no Rails helpers needed)
|
16
|
+
1) Display container (required)
|
17
|
+
<div data-flash-message-container></div>
|
18
|
+
|
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>
|
26
|
+
|
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>
|
35
|
+
|
36
|
+
4) Global storage (required by appendMessageToStorage)
|
37
|
+
<div id="flash-storage" style="display:none;"></div>
|
38
|
+
|
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
|
+
}
|
49
|
+
|
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();
|
56
|
+
});
|
57
|
+
*/
|
58
|
+
|
59
|
+
/* 初回描画リスナーをセットします。
|
60
|
+
DOMContentLoaded 時に renderFlashMessages() を一度だけ呼びます。
|
61
|
+
---
|
62
|
+
Install a listener to render flash messages on DOMContentLoaded (once).
|
63
|
+
*/
|
64
|
+
function installInitialRenderListener() {
|
65
|
+
if (document.readyState === 'loading') {
|
66
|
+
document.addEventListener('DOMContentLoaded', function() { renderFlashMessages(); }, { once: true });
|
67
|
+
} else {
|
68
|
+
renderFlashMessages();
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
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
|
+
*/
|
80
|
+
function renderFlashMessages() {
|
81
|
+
const storages = document.querySelectorAll('[data-flash-storage]');
|
82
|
+
const containers = document.querySelectorAll('[data-flash-message-container]');
|
83
|
+
|
84
|
+
// Aggregated messages list
|
85
|
+
const messages = [];
|
86
|
+
storages.forEach(storage => {
|
87
|
+
const ul = storage.querySelector('ul');
|
88
|
+
if (ul && ul.children.length > 0) {
|
89
|
+
ul.querySelectorAll('li').forEach(li => {
|
90
|
+
messages.push({ type: li.dataset.type || 'notice', message: li.textContent.trim() });
|
91
|
+
});
|
92
|
+
}
|
93
|
+
// Remove storage after consuming
|
94
|
+
storage.remove();
|
95
|
+
});
|
96
|
+
|
97
|
+
containers.forEach(container => {
|
98
|
+
messages.forEach(({ type, message }) => {
|
99
|
+
if (message) container.appendChild(createFlashMessageNode(type, message));
|
100
|
+
});
|
101
|
+
});
|
102
|
+
}
|
103
|
+
|
104
|
+
/* フラッシュ・メッセージ項目として message をデータとして埋め込みます。
|
105
|
+
埋め込まれた項目は renderFlashMessages を呼び出すことによって表示されます。
|
106
|
+
---
|
107
|
+
Append a message item into the hidden storage.
|
108
|
+
Call renderFlashMessages() to display it.
|
109
|
+
*/
|
110
|
+
function appendMessageToStorage(message, type = 'notice') {
|
111
|
+
const storageContainer = document.getElementById("flash-storage");
|
112
|
+
if (!storageContainer) {
|
113
|
+
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
|
+
return;
|
121
|
+
}
|
122
|
+
|
123
|
+
let storage = storageContainer.querySelector('[data-flash-storage]');
|
124
|
+
if (!storage) {
|
125
|
+
storage = document.createElement('div');
|
126
|
+
storage.setAttribute('data-flash-storage', 'true');
|
127
|
+
storage.style.display = 'none';
|
128
|
+
storageContainer.appendChild(storage);
|
129
|
+
}
|
130
|
+
|
131
|
+
let ul = storage.querySelector('ul');
|
132
|
+
if (!ul) {
|
133
|
+
ul = document.createElement('ul');
|
134
|
+
storage.appendChild(ul);
|
135
|
+
}
|
136
|
+
|
137
|
+
const li = document.createElement('li');
|
138
|
+
li.dataset.type = type;
|
139
|
+
li.textContent = message;
|
140
|
+
ul.appendChild(li);
|
141
|
+
}
|
142
|
+
|
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
|
+
*/
|
149
|
+
function installCustomEventListener() {
|
150
|
+
const root = document.documentElement;
|
151
|
+
if (root.hasAttribute('data-flash-unified-custom-listener')) return; // idempotent
|
152
|
+
root.setAttribute('data-flash-unified-custom-listener', 'true');
|
153
|
+
|
154
|
+
document.addEventListener('flash-unified:messages', function(event) {
|
155
|
+
try {
|
156
|
+
processMessagePayload(event.detail);
|
157
|
+
} catch (e) {
|
158
|
+
console.error('[FlashUnified] Failed to handle custom payload', e);
|
159
|
+
}
|
160
|
+
});
|
161
|
+
}
|
162
|
+
|
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
|
+
*/
|
170
|
+
function clearFlashMessages(message) {
|
171
|
+
document.querySelectorAll('[data-flash-message-container]').forEach(container => {
|
172
|
+
// メッセージ指定なし: メッセージ要素のみ全削除(コンテナ内の他要素は残す)
|
173
|
+
if (typeof message === 'undefined') {
|
174
|
+
container.querySelectorAll('[data-flash-message]')?.forEach(n => n.remove());
|
175
|
+
return;
|
176
|
+
}
|
177
|
+
|
178
|
+
// 指定メッセージに一致する要素だけ削除
|
179
|
+
container.querySelectorAll('[data-flash-message]')?.forEach(n => {
|
180
|
+
const text = n.querySelector('.flash-message-text');
|
181
|
+
if (text && text.textContent.trim() === message) n.remove();
|
182
|
+
});
|
183
|
+
});
|
184
|
+
}
|
185
|
+
|
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
|
+
*/
|
195
|
+
function createFlashMessageNode(type, message) {
|
196
|
+
const templateId = `flash-message-template-${type}`;
|
197
|
+
const template = document.getElementById(templateId);
|
198
|
+
if (template && template.content) {
|
199
|
+
const base = template.content.firstElementChild;
|
200
|
+
if (!base) {
|
201
|
+
console.error(`[FlashUnified] Template #${templateId} has no root element`);
|
202
|
+
const node = document.createElement('div');
|
203
|
+
node.setAttribute('role', 'alert');
|
204
|
+
node.setAttribute('data-flash-message', 'true');
|
205
|
+
node.textContent = message;
|
206
|
+
return node;
|
207
|
+
}
|
208
|
+
const root = base.cloneNode(true);
|
209
|
+
root.setAttribute('data-flash-message', 'true');
|
210
|
+
const span = root.querySelector('.flash-message-text');
|
211
|
+
if (span) span.textContent = message;
|
212
|
+
return root;
|
213
|
+
} else {
|
214
|
+
console.error(`[FlashUnified] No template found for type: ${type}`);
|
215
|
+
// テンプレートがない場合は生成 / Fallback element when template is missing
|
216
|
+
const node = document.createElement('div');
|
217
|
+
node.setAttribute('role', 'alert');
|
218
|
+
node.setAttribute('data-flash-message', 'true');
|
219
|
+
const span = document.createElement('span');
|
220
|
+
span.className = 'flash-message-text';
|
221
|
+
span.textContent = message;
|
222
|
+
node.appendChild(span);
|
223
|
+
return node;
|
224
|
+
}
|
225
|
+
}
|
226
|
+
|
227
|
+
/* 何らかのストレージにメッセージが存在するかを判定します。
|
228
|
+
---
|
229
|
+
Return true if any [data-flash-storage] contains at least one <li> item.
|
230
|
+
*/
|
231
|
+
function storageHasMessages() {
|
232
|
+
const storages = document.querySelectorAll('[data-flash-storage]');
|
233
|
+
for (const storage of storages) {
|
234
|
+
const ul = storage.querySelector('ul');
|
235
|
+
if (ul && ul.children.length > 0) {
|
236
|
+
return true;
|
237
|
+
}
|
238
|
+
}
|
239
|
+
return false;
|
240
|
+
}
|
241
|
+
|
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
|
+
*/
|
247
|
+
function processMessagePayload(payload) {
|
248
|
+
if (!payload) return;
|
249
|
+
const list = Array.isArray(payload)
|
250
|
+
? payload
|
251
|
+
: (Array.isArray(payload.messages) ? payload.messages : []);
|
252
|
+
if (list.length === 0) return;
|
253
|
+
list.forEach(({ type, message }) => {
|
254
|
+
if (!message) return;
|
255
|
+
appendMessageToStorage(String(message), type);
|
256
|
+
});
|
257
|
+
renderFlashMessages();
|
258
|
+
}
|
259
|
+
|
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
|
+
*/
|
267
|
+
function startMutationObserver() {
|
268
|
+
const root = document.documentElement;
|
269
|
+
if (root.hasAttribute('data-flash-unified-observer-enabled')) return;
|
270
|
+
root.setAttribute('data-flash-unified-observer-enabled', 'true');
|
271
|
+
|
272
|
+
const observer = new MutationObserver((mutations) => {
|
273
|
+
let shouldRender = false;
|
274
|
+
for (const m of mutations) {
|
275
|
+
if (m.type === 'childList') {
|
276
|
+
m.addedNodes.forEach((node) => {
|
277
|
+
if (!(node instanceof Element)) return;
|
278
|
+
if (node.matches('[data-flash-storage], [data-flash-message-container], template[id^="flash-message-template-"]')) {
|
279
|
+
shouldRender = true;
|
280
|
+
}
|
281
|
+
if (node.querySelector && node.querySelector('[data-flash-storage]')) {
|
282
|
+
shouldRender = true;
|
283
|
+
}
|
284
|
+
});
|
285
|
+
}
|
286
|
+
}
|
287
|
+
if (shouldRender) {
|
288
|
+
renderFlashMessages();
|
289
|
+
}
|
290
|
+
});
|
291
|
+
|
292
|
+
observer.observe(document.body, {
|
293
|
+
childList: true,
|
294
|
+
subtree: true
|
295
|
+
});
|
296
|
+
}
|
297
|
+
|
298
|
+
export {
|
299
|
+
renderFlashMessages,
|
300
|
+
appendMessageToStorage,
|
301
|
+
clearFlashMessages,
|
302
|
+
processMessagePayload,
|
303
|
+
startMutationObserver,
|
304
|
+
installCustomEventListener,
|
305
|
+
installInitialRenderListener,
|
306
|
+
storageHasMessages
|
307
|
+
};
|
@@ -0,0 +1,83 @@
|
|
1
|
+
/*
|
2
|
+
Flash Unified — Network Error Helpers
|
3
|
+
|
4
|
+
Purpose:
|
5
|
+
- Provide optional network and HTTP error handling for Turbo form submissions.
|
6
|
+
- Users can install these listeners if they want automatic error message display.
|
7
|
+
|
8
|
+
Required DOM:
|
9
|
+
<ul id="general-error-messages" style="display:none;">
|
10
|
+
<li data-status="413">Payload Too Large</li>
|
11
|
+
<li data-status="network">Network Error</li>
|
12
|
+
<li data-status="500">Internal Server Error</li>
|
13
|
+
</ul>
|
14
|
+
|
15
|
+
Usage:
|
16
|
+
// Framework-agnostic helpers (call from your own handlers)
|
17
|
+
import { notifyNetworkError, notifyHttpError } from "flash_unified/network_helpers";
|
18
|
+
notifyNetworkError(); // Add generic network error and render
|
19
|
+
notifyHttpError(413); // Add HTTP-specific message and render
|
20
|
+
*/
|
21
|
+
|
22
|
+
import { renderFlashMessages, appendMessageToStorage, storageHasMessages } from 'flash_unified';
|
23
|
+
|
24
|
+
/* エラーステータスに応じた汎用メッセージをストレージへ追加します。
|
25
|
+
既にストレージにメッセージが存在する場合は何もしません。
|
26
|
+
'network' または 4xx/5xx を対象とし、#general-error-messages から文言を解決します。
|
27
|
+
---
|
28
|
+
Add a general error message to storage based on status ('network' or 4xx/5xx).
|
29
|
+
If any storage already has messages, this is a no-op. Looks up text in #general-error-messages.
|
30
|
+
*/
|
31
|
+
function resolveAndAppendErrorMessage(status) {
|
32
|
+
// If any flash storage already contains messages, do not override it
|
33
|
+
if (storageHasMessages()) return;
|
34
|
+
|
35
|
+
// API: resolveAndAppendErrorMessage expects a numeric HTTP status code.
|
36
|
+
// Use 0 to indicate a network-level failure (CORS, network down, file://, etc.).
|
37
|
+
// Non-error codes (< 400) are ignored.
|
38
|
+
const num = Number(status);
|
39
|
+
if (isNaN(num)) return; // invalid (non-numeric)
|
40
|
+
if (num < 0) return; // negative numbers not expected
|
41
|
+
if (num > 0 && num < 400) return; // non-error HTTP status codes
|
42
|
+
|
43
|
+
// Determine lookup key
|
44
|
+
let key;
|
45
|
+
if (num === 0) {
|
46
|
+
key = 'network';
|
47
|
+
} else {
|
48
|
+
key = String(num);
|
49
|
+
}
|
50
|
+
|
51
|
+
// Avoid duplicates when container has children
|
52
|
+
const container = document.querySelector('[data-flash-message-container]');
|
53
|
+
if (container && container.querySelector('[data-flash-message]')) return;
|
54
|
+
|
55
|
+
const generalerrors = document.getElementById('general-error-messages');
|
56
|
+
if (!generalerrors) {
|
57
|
+
console.error('[FlashUnified] No general error messages element found');
|
58
|
+
return;
|
59
|
+
}
|
60
|
+
|
61
|
+
const li = generalerrors.querySelector(`li[data-status="${key}"]`);
|
62
|
+
if (li) {
|
63
|
+
appendMessageToStorage(li.textContent.trim(), 'alert');
|
64
|
+
} else {
|
65
|
+
console.error(`[FlashUnified] No error message defined for status: ${status}`);
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
function notifyNetworkError() {
|
70
|
+
resolveAndAppendErrorMessage(0);
|
71
|
+
renderFlashMessages();
|
72
|
+
}
|
73
|
+
|
74
|
+
function notifyHttpError(status) {
|
75
|
+
resolveAndAppendErrorMessage(status);
|
76
|
+
renderFlashMessages();
|
77
|
+
}
|
78
|
+
|
79
|
+
export {
|
80
|
+
resolveAndAppendErrorMessage,
|
81
|
+
notifyNetworkError,
|
82
|
+
notifyHttpError
|
83
|
+
};
|
@@ -0,0 +1,135 @@
|
|
1
|
+
/*
|
2
|
+
Flash Unified — Turbo Integration Helpers
|
3
|
+
|
4
|
+
Purpose:
|
5
|
+
- Provide optional Turbo event listeners for automatic flash message rendering.
|
6
|
+
- Users can install these listeners if they want automatic integration with Turbo.
|
7
|
+
|
8
|
+
Usage:
|
9
|
+
import { renderFlashMessages } from "flash_unified";
|
10
|
+
import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
|
11
|
+
|
12
|
+
// Install automatic Turbo event listeners
|
13
|
+
installTurboRenderListeners();
|
14
|
+
|
15
|
+
// For the initial render we recommend using the core helper:
|
16
|
+
// import { installInitialRenderListener } from "flash_unified";
|
17
|
+
// installInitialRenderListener();
|
18
|
+
*/
|
19
|
+
|
20
|
+
import { renderFlashMessages, installCustomEventListener } from 'flash_unified';
|
21
|
+
import { resolveAndAppendErrorMessage } from 'flash_unified/network_helpers';
|
22
|
+
|
23
|
+
/* Turbo関連のイベントリスナーを設定します。
|
24
|
+
ページ遷移やフレーム更新時に自動的にフラッシュメッセージを描画します。
|
25
|
+
---
|
26
|
+
Install Turbo event listeners for automatic flash message rendering.
|
27
|
+
Renders messages on page navigation and frame updates.
|
28
|
+
*/
|
29
|
+
function installTurboRenderListeners() {
|
30
|
+
const root = document.documentElement;
|
31
|
+
if (root.hasAttribute('data-flash-unified-turbo-listeners')) {
|
32
|
+
return; // Already installed
|
33
|
+
}
|
34
|
+
root.setAttribute('data-flash-unified-turbo-listeners', 'true');
|
35
|
+
|
36
|
+
// Turbo page load events
|
37
|
+
document.addEventListener('turbo:load', function() {
|
38
|
+
renderFlashMessages();
|
39
|
+
});
|
40
|
+
|
41
|
+
document.addEventListener('turbo:frame-load', function() {
|
42
|
+
renderFlashMessages();
|
43
|
+
});
|
44
|
+
|
45
|
+
document.addEventListener('turbo:render', function() {
|
46
|
+
renderFlashMessages();
|
47
|
+
});
|
48
|
+
|
49
|
+
// Turbo Stream events
|
50
|
+
installTurboStreamEvents();
|
51
|
+
}
|
52
|
+
|
53
|
+
/* Turbo Stream更新後のカスタムイベントを設定します。
|
54
|
+
turbo:before-stream-renderをフックして描画完了後にイベントを発火させます。
|
55
|
+
---
|
56
|
+
Setup custom turbo:after-stream-render event for Turbo Stream updates.
|
57
|
+
Hooks into turbo:before-stream-render to dispatch event after rendering is done.
|
58
|
+
*/
|
59
|
+
// Internal: used by installTurboRenderListeners
|
60
|
+
function installTurboStreamEvents() {
|
61
|
+
// Create custom event for after stream render
|
62
|
+
const afterRenderEvent = new Event("turbo:after-stream-render");
|
63
|
+
|
64
|
+
// Hook into turbo:before-stream-render to add our custom event
|
65
|
+
document.addEventListener("turbo:before-stream-render", (event) => {
|
66
|
+
const originalRender = event.detail.render;
|
67
|
+
event.detail.render = async function (streamElement) {
|
68
|
+
await originalRender(streamElement);
|
69
|
+
document.dispatchEvent(afterRenderEvent);
|
70
|
+
};
|
71
|
+
});
|
72
|
+
|
73
|
+
// Listen for our custom after-stream-render event
|
74
|
+
document.addEventListener("turbo:after-stream-render", function() {
|
75
|
+
renderFlashMessages();
|
76
|
+
});
|
77
|
+
}
|
78
|
+
|
79
|
+
/* Turboリスナー + カスタムイベントリスナーを一度に設定します。
|
80
|
+
---
|
81
|
+
Sets up Turbo listeners + custom event listeners in one call.
|
82
|
+
*/
|
83
|
+
function installTurboIntegration() {
|
84
|
+
const root = document.documentElement;
|
85
|
+
if (root.hasAttribute('data-flash-unified-initialized')) return; // idempotent
|
86
|
+
root.setAttribute('data-flash-unified-initialized', 'true');
|
87
|
+
|
88
|
+
// Delegate to existing installers to avoid duplicating logic.
|
89
|
+
installTurboRenderListeners();
|
90
|
+
installCustomEventListener();
|
91
|
+
}
|
92
|
+
|
93
|
+
export {
|
94
|
+
installTurboRenderListeners,
|
95
|
+
installTurboIntegration
|
96
|
+
};
|
97
|
+
|
98
|
+
/* ネットワークエラー関連のイベントリスナーを設定します。
|
99
|
+
Turboフォーム送信時のエラー処理を自動化します。
|
100
|
+
---
|
101
|
+
Install network error event listeners for automatic error handling.
|
102
|
+
Handles Turbo form submission errors and network issues.
|
103
|
+
*/
|
104
|
+
function installNetworkErrorListeners() {
|
105
|
+
const root = document.documentElement;
|
106
|
+
if (root.hasAttribute('data-flash-unified-network-listeners')) {
|
107
|
+
return; // Already installed
|
108
|
+
}
|
109
|
+
root.setAttribute('data-flash-unified-network-listeners', 'true');
|
110
|
+
|
111
|
+
document.addEventListener('turbo:submit-end', function(event) {
|
112
|
+
const res = event.detail.fetchResponse;
|
113
|
+
// Determine a numeric status code to pass to the resolver.
|
114
|
+
// Use 0 to represent a network-level failure where no response was received.
|
115
|
+
let statusCode;
|
116
|
+
if (res === undefined) {
|
117
|
+
statusCode = 0;
|
118
|
+
console.warn('[FlashUnified] No response received from server. Possible network or proxy error.');
|
119
|
+
} else {
|
120
|
+
statusCode = res.statusCode;
|
121
|
+
}
|
122
|
+
|
123
|
+
resolveAndAppendErrorMessage(statusCode);
|
124
|
+
renderFlashMessages();
|
125
|
+
});
|
126
|
+
|
127
|
+
document.addEventListener('turbo:fetch-request-error', function(_event) {
|
128
|
+
// Treat fetch-request-error as a network-level failure (status 0)
|
129
|
+
const statusCode = 0;
|
130
|
+
resolveAndAppendErrorMessage(statusCode);
|
131
|
+
renderFlashMessages();
|
132
|
+
});
|
133
|
+
}
|
134
|
+
|
135
|
+
export { installNetworkErrorListeners };
|
@@ -0,0 +1 @@
|
|
1
|
+
<div data-flash-message-container></div>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<ul id="general-error-messages" style="display: none;">
|
2
|
+
<% status_codes = %w[
|
3
|
+
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
|
4
|
+
421 422 423 424 425 426 428 429 431 451
|
5
|
+
500 501 502 503 504 505 506 507 508 510 511
|
6
|
+
network
|
7
|
+
] %>
|
8
|
+
|
9
|
+
<% status_codes.each do |code| %>
|
10
|
+
<li data-status="<%= code %>"><%= t("http_status_messages.#{code}") %></li>
|
11
|
+
<% end %>
|
12
|
+
</ul>
|
@@ -0,0 +1 @@
|
|
1
|
+
<div id="flash-storage" style="display: none;"></div>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<template id="flash-message-template-notice">
|
2
|
+
<div class="flash-notice" role="alert">
|
3
|
+
<span class="flash-message-text"></span>
|
4
|
+
</div>
|
5
|
+
</template>
|
6
|
+
<template id="flash-message-template-warning">
|
7
|
+
<div class="flash-warning" role="alert">
|
8
|
+
<span class="flash-message-text"></span>
|
9
|
+
</div>
|
10
|
+
</template>
|
11
|
+
<template id="flash-message-template-alert">
|
12
|
+
<div class="flash-alert" role="alert">
|
13
|
+
<span class="flash-message-text"></span>
|
14
|
+
</div>
|
15
|
+
</template>
|
16
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
en:
|
2
|
+
http_status_messages:
|
3
|
+
"400": "Bad Request"
|
4
|
+
"401": "Unauthorized"
|
5
|
+
"402": "Payment Required"
|
6
|
+
"403": "Forbidden"
|
7
|
+
"404": "Not Found"
|
8
|
+
"405": "Method Not Allowed"
|
9
|
+
"406": "Not Acceptable"
|
10
|
+
"407": "Proxy Authentication Required"
|
11
|
+
"408": "Request Timeout"
|
12
|
+
"409": "Conflict"
|
13
|
+
"410": "Gone"
|
14
|
+
"411": "Length Required"
|
15
|
+
"412": "Precondition Failed"
|
16
|
+
"413": "Payload Too Large"
|
17
|
+
"414": "URI Too Long"
|
18
|
+
"415": "Unsupported Media Type"
|
19
|
+
"416": "Range Not Satisfiable"
|
20
|
+
"417": "Expectation Failed"
|
21
|
+
"418": "I'm a teapot"
|
22
|
+
"421": "Misdirected Request"
|
23
|
+
"422": "Unprocessable Entity"
|
24
|
+
"423": "Locked"
|
25
|
+
"424": "Failed Dependency"
|
26
|
+
"425": "Too Early"
|
27
|
+
"426": "Upgrade Required"
|
28
|
+
"428": "Precondition Required"
|
29
|
+
"429": "Too Many Requests"
|
30
|
+
"431": "Request Header Fields Too Large"
|
31
|
+
"451": "Unavailable For Legal Reasons"
|
32
|
+
"500": "Internal Server Error"
|
33
|
+
"501": "Not Implemented"
|
34
|
+
"502": "Bad Gateway"
|
35
|
+
"503": "Service Unavailable"
|
36
|
+
"504": "Gateway Timeout"
|
37
|
+
"505": "HTTP Version Not Supported"
|
38
|
+
"506": "Variant Also Negotiates"
|
39
|
+
"507": "Insufficient Storage"
|
40
|
+
"508": "Loop Detected"
|
41
|
+
"510": "Not Extended"
|
42
|
+
"511": "Network Authentication Required"
|
43
|
+
network: "Network Error"
|
@@ -0,0 +1,43 @@
|
|
1
|
+
ja:
|
2
|
+
http_status_messages:
|
3
|
+
"400": "不正なリクエスト"
|
4
|
+
"401": "認証が必要です"
|
5
|
+
"402": "支払いが必要です"
|
6
|
+
"403": "アクセスが禁止されています"
|
7
|
+
"404": "見つかりません"
|
8
|
+
"405": "許可されていないメソッドです"
|
9
|
+
"406": "許可されていない形式です"
|
10
|
+
"407": "プロキシ認証が必要です"
|
11
|
+
"408": "リクエストタイムアウト"
|
12
|
+
"409": "競合があります"
|
13
|
+
"410": "消失しました"
|
14
|
+
"411": "長さが必要です"
|
15
|
+
"412": "前提条件に失敗しました"
|
16
|
+
"413": "ペイロードが大きすぎます"
|
17
|
+
"414": "URI が長すぎます"
|
18
|
+
"415": "サポートされていないメディアタイプ"
|
19
|
+
"416": "レンジが満たされません"
|
20
|
+
"417": "期待に失敗しました"
|
21
|
+
"418": "I'm a teapot"
|
22
|
+
"421": "誤った宛先へのリクエスト"
|
23
|
+
"422": "処理できないエンティティ"
|
24
|
+
"423": "ロックされています"
|
25
|
+
"424": "依存関係に失敗しました"
|
26
|
+
"425": "早すぎます"
|
27
|
+
"426": "アップグレードが必要です"
|
28
|
+
"428": "前提条件が必要です"
|
29
|
+
"429": "リクエストが多すぎます"
|
30
|
+
"431": "リクエストヘッダが大きすぎます"
|
31
|
+
"451": "法的理由により利用不可"
|
32
|
+
"500": "サーバ内部エラー"
|
33
|
+
"501": "実装されていません"
|
34
|
+
"502": "不正なゲートウェイ"
|
35
|
+
"503": "サービス利用不可"
|
36
|
+
"504": "ゲートウェイタイムアウト"
|
37
|
+
"505": "HTTP バージョン非サポート"
|
38
|
+
"506": "代替表現も交渉しています"
|
39
|
+
"507": "保存容量が不足しています"
|
40
|
+
"508": "ループ検出"
|
41
|
+
"510": "拡張されていません"
|
42
|
+
"511": "ネットワーク認証が必要です"
|
43
|
+
network: "ネットワークエラー"
|