comfy_middle_seat 3.2.0 → 4.0.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.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/coveralls.yml +4 -0
  3. data/CHANGELOG.md +56 -3
  4. data/CONTRIBUTING.md +8 -5
  5. data/Gemfile +1 -1
  6. data/README.md +66 -29
  7. data/Rakefile +3 -3
  8. data/app/assets/javascripts/comfy/admin/cms/application.js +2 -0
  9. data/app/assets/javascripts/comfy/admin/cms/base.js +6 -1
  10. data/app/assets/javascripts/comfy/admin/cms/codemirror.js +17 -0
  11. data/app/assets/javascripts/comfy/admin/cms/dirty_forms.js +201 -0
  12. data/app/assets/javascripts/comfy/admin/cms/draft_forms.js +468 -0
  13. data/app/assets/javascripts/comfy/admin/cms/page_fragments.js +393 -22
  14. data/app/assets/javascripts/comfy/admin/cms/wysiwyg.js +21 -0
  15. data/app/assets/javascripts/comfy/vendor/redactor/filemanager.js +48 -30
  16. data/app/assets/javascripts/comfy/vendor/redactor/imagemanager.js +43 -25
  17. data/app/assets/stylesheets/comfy/admin/cms/base.sass +23 -0
  18. data/app/controllers/comfy/admin/base_controller.rb +3 -3
  19. data/app/controllers/comfy/admin/cms/base_controller.rb +12 -7
  20. data/app/controllers/comfy/admin/cms/files_controller.rb +1 -1
  21. data/app/controllers/comfy/admin/cms/pages_controller.rb +76 -16
  22. data/app/controllers/comfy/admin/cms/revisions/base_controller.rb +39 -1
  23. data/app/controllers/comfy/admin/cms/revisions/page_controller.rb +9 -8
  24. data/app/controllers/comfy/admin/cms/revisions/translation_controller.rb +14 -8
  25. data/app/controllers/comfy/admin/cms/sites_controller.rb +1 -1
  26. data/app/controllers/comfy/admin/cms/translations_controller.rb +3 -1
  27. data/app/controllers/comfy/cms/base_controller.rb +1 -1
  28. data/app/controllers/comfy/cms/content_controller.rb +5 -5
  29. data/app/controllers/concerns/comfy/reorder_action.rb +24 -4
  30. data/app/helpers/comfy/admin/cms_helper.rb +28 -3
  31. data/app/helpers/comfy/cms_helper.rb +1 -1
  32. data/app/models/comfy/cms/file.rb +25 -3
  33. data/app/models/comfy/cms/layout.rb +8 -6
  34. data/app/models/comfy/cms/page.rb +22 -0
  35. data/app/models/comfy/cms/site.rb +3 -3
  36. data/app/models/comfy/cms/snippet.rb +8 -3
  37. data/app/models/concerns/comfy/cms/with_fragments.rb +2 -2
  38. data/app/views/comfy/admin/cms/categories/_index.html.haml +1 -1
  39. data/app/views/comfy/admin/cms/categories/_show.html.haml +1 -1
  40. data/app/views/comfy/admin/cms/files/_file.html.haml +1 -1
  41. data/app/views/comfy/admin/cms/files/index.html.haml +9 -2
  42. data/app/views/comfy/admin/cms/fragments/_form_fragments.html.haml +2 -2
  43. data/app/views/comfy/admin/cms/pages/_form.html.haml +2 -1
  44. data/app/views/comfy/admin/cms/pages/_index_branch.html.haml +11 -10
  45. data/app/views/comfy/admin/cms/pages/edit.html.haml +3 -1
  46. data/app/views/comfy/admin/cms/pages/index.html.haml +9 -2
  47. data/app/views/comfy/admin/cms/pages/new.html.haml +3 -1
  48. data/app/views/comfy/admin/cms/pages/toggle_branch.js.erb +2 -2
  49. data/app/views/comfy/admin/cms/revisions/show.html.haml +25 -7
  50. data/app/views/comfy/admin/cms/shared/_search.html.haml +9 -0
  51. data/app/views/comfy/admin/cms/sites/_form.html.haml +1 -1
  52. data/app/views/comfy/admin/cms/translations/_form.html.haml +2 -1
  53. data/app/views/comfy/admin/cms/translations/edit.html.haml +3 -1
  54. data/app/views/comfy/admin/cms/translations/new.html.haml +3 -1
  55. data/app/views/layouts/comfy/admin/cms/_flash.html.haml +1 -1
  56. data/app/views/layouts/comfy/admin/cms/_head.html.haml +3 -1
  57. data/app/views/layouts/comfy/admin/cms/_left.html.haml +2 -2
  58. data/comfy_middle_seat.gemspec +10 -5
  59. data/config/application.rb +3 -2
  60. data/config/cms_routes.rb +1 -1
  61. data/config/environment.rb +1 -1
  62. data/config/environments/development.rb +1 -1
  63. data/config/environments/production.rb +1 -1
  64. data/config/environments/test.rb +1 -1
  65. data/config/initializers/{comfortable_media_surfer.rb → comfy_middle_seat.rb} +4 -4
  66. data/config/locales/ar.yml +14 -0
  67. data/config/locales/ca.yml +14 -0
  68. data/config/locales/cs.yml +14 -0
  69. data/config/locales/da.yml +14 -0
  70. data/config/locales/de.yml +14 -0
  71. data/config/locales/en.yml +14 -0
  72. data/config/locales/es.yml +14 -0
  73. data/config/locales/fi.yml +14 -0
  74. data/config/locales/fr.yml +14 -0
  75. data/config/locales/gr.yml +14 -0
  76. data/config/locales/hr.yml +14 -0
  77. data/config/locales/it.yml +14 -0
  78. data/config/locales/ja.yml +14 -0
  79. data/config/locales/nb.yml +14 -0
  80. data/config/locales/nl.yml +14 -0
  81. data/config/locales/pl.yml +14 -0
  82. data/config/locales/pt-BR.yml +14 -0
  83. data/config/locales/ru.yml +14 -0
  84. data/config/locales/sk.yml +14 -0
  85. data/config/locales/sv.yml +14 -0
  86. data/config/locales/tr.yml +14 -0
  87. data/config/locales/uk.yml +14 -0
  88. data/config/locales/zh-CN.yml +14 -0
  89. data/config/locales/zh-TW.yml +14 -0
  90. data/config.ru +1 -1
  91. data/exe/comfy-middle-seat-upgrade +34 -0
  92. data/lib/{comfortable_media_surfer → comfy_middle_seat}/access_control/admin_authentication.rb +4 -4
  93. data/lib/{comfortable_media_surfer → comfy_middle_seat}/access_control/admin_authorization.rb +1 -1
  94. data/lib/{comfortable_media_surfer → comfy_middle_seat}/access_control/public_authentication.rb +1 -1
  95. data/lib/{comfortable_media_surfer → comfy_middle_seat}/access_control/public_authorization.rb +1 -1
  96. data/lib/{comfortable_media_surfer → comfy_middle_seat}/configuration.rb +7 -7
  97. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/block.rb +1 -1
  98. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/params_parser.rb +1 -1
  99. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/renderer.rb +10 -10
  100. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tag.rb +4 -4
  101. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/asset.rb +4 -4
  102. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/audio.rb +3 -3
  103. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/breadcrumbs.rb +3 -3
  104. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/checkbox.rb +3 -3
  105. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/children.rb +3 -3
  106. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/date.rb +3 -3
  107. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/datetime.rb +3 -3
  108. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/file.rb +5 -5
  109. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/file_link.rb +4 -4
  110. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/files.rb +3 -3
  111. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/fragment.rb +2 -2
  112. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/helper.rb +4 -4
  113. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/image.rb +4 -4
  114. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/markdown.rb +3 -3
  115. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/mixins/file_content.rb +1 -1
  116. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/number.rb +3 -3
  117. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/page_file_link.rb +5 -5
  118. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/partial.rb +4 -4
  119. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/siblings.rb +3 -3
  120. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/snippet.rb +3 -3
  121. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/template.rb +4 -4
  122. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/text.rb +3 -3
  123. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/textarea.rb +3 -3
  124. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags/wysiwyg.rb +3 -3
  125. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content/tags.rb +1 -1
  126. data/lib/{comfortable_media_surfer → comfy_middle_seat}/content.rb +1 -1
  127. data/lib/{comfortable_media_surfer → comfy_middle_seat}/engine.rb +4 -4
  128. data/lib/{comfortable_media_surfer → comfy_middle_seat}/error.rb +4 -4
  129. data/lib/{comfortable_media_surfer → comfy_middle_seat}/extensions/acts_as_tree.rb +3 -3
  130. data/lib/{comfortable_media_surfer → comfy_middle_seat}/extensions/has_revisions.rb +4 -4
  131. data/lib/{comfortable_media_surfer → comfy_middle_seat}/extensions.rb +1 -1
  132. data/lib/{comfortable_media_surfer → comfy_middle_seat}/form_builder.rb +2 -2
  133. data/lib/comfy_middle_seat/initializer_upgrader.rb +115 -0
  134. data/lib/{comfortable_media_surfer → comfy_middle_seat}/render_methods.rb +7 -7
  135. data/lib/{comfortable_media_surfer → comfy_middle_seat}/routing.rb +3 -2
  136. data/lib/{comfortable_media_surfer → comfy_middle_seat}/seeds/file/exporter.rb +5 -5
  137. data/lib/{comfortable_media_surfer → comfy_middle_seat}/seeds/file/importer.rb +5 -5
  138. data/lib/{comfortable_media_surfer → comfy_middle_seat}/seeds/layout/exporter.rb +4 -4
  139. data/lib/{comfortable_media_surfer → comfy_middle_seat}/seeds/layout/importer.rb +5 -5
  140. data/lib/{comfortable_media_surfer → comfy_middle_seat}/seeds/page/exporter.rb +5 -5
  141. data/lib/{comfortable_media_surfer → comfy_middle_seat}/seeds/page/importer.rb +7 -7
  142. data/lib/{comfortable_media_surfer → comfy_middle_seat}/seeds/snippet/exporter.rb +4 -4
  143. data/lib/{comfortable_media_surfer → comfy_middle_seat}/seeds/snippet/importer.rb +5 -5
  144. data/lib/{comfortable_media_surfer → comfy_middle_seat}/seeds.rb +4 -4
  145. data/lib/comfy_middle_seat/version.rb +2 -2
  146. data/lib/{comfortable_media_surfer → comfy_middle_seat}/view_hooks.rb +2 -2
  147. data/lib/comfy_middle_seat.rb +54 -1
  148. data/lib/generators/comfy/cms/README +3 -11
  149. data/lib/generators/comfy/cms/cms_generator.rb +2 -2
  150. data/lib/generators/comfy/scaffold/templates/tests/controller.rb.tt +2 -2
  151. data/lib/tasks/cms_seeds.rake +8 -8
  152. data/package-lock.json +2 -2
  153. data/package.json +1 -1
  154. metadata +68 -62
  155. data/lib/comfortable_media_surfer/version.rb +0 -3
  156. data/lib/comfortable_media_surfer.rb +0 -56
@@ -0,0 +1,468 @@
1
+ import Modal from "bootstrap/js/src/modal";
2
+
3
+ (() => {
4
+ const VERSION = 1;
5
+ const DEBOUNCE_DELAY = 500;
6
+ const fragmentIdentifierName =
7
+ /\[fragments_attributes\]\[\d+\]\[identifier\]$/;
8
+ const ignoredNames = new Set([
9
+ "authenticity_token",
10
+ "cms_draft_key",
11
+ "_method",
12
+ "utf8",
13
+ ]);
14
+
15
+ let forms = [];
16
+ let timers = new Map();
17
+ let changedForms = new Set();
18
+ let suspendedForms = new Set();
19
+ let formHandlers = new Map();
20
+ let baselines = new WeakMap();
21
+ let prompt = null;
22
+
23
+ const safely = (operation, fallback = null) => {
24
+ try {
25
+ return operation();
26
+ } catch (error) {
27
+ console.warn("CMS draft storage is unavailable", error);
28
+ return fallback;
29
+ }
30
+ };
31
+
32
+ const readStoredValue = (storageName, key) =>
33
+ safely(() => window[storageName].getItem(key));
34
+
35
+ const writeStoredValue = (storageName, key, value) =>
36
+ safely(() => {
37
+ window[storageName].setItem(key, value);
38
+ return true;
39
+ }, false);
40
+
41
+ const removeStoredValue = (storageName, key) =>
42
+ safely(() => {
43
+ window[storageName].removeItem(key);
44
+ return true;
45
+ }, false);
46
+
47
+ const parseStoredValue = (value) => {
48
+ if (!value) return null;
49
+
50
+ return safely(() => JSON.parse(value));
51
+ };
52
+
53
+ const syncEditors = (root) => {
54
+ if (CMS.codemirror && CMS.codemirror.sync) CMS.codemirror.sync(root);
55
+ if (CMS.wysiwyg && CMS.wysiwyg.sync) CMS.wysiwyg.sync(root);
56
+ };
57
+
58
+ const restoreEditors = (root) => {
59
+ if (CMS.codemirror && CMS.codemirror.restore) {
60
+ CMS.codemirror.restore(root);
61
+ }
62
+ if (CMS.wysiwyg && CMS.wysiwyg.restore) CMS.wysiwyg.restore(root);
63
+ };
64
+
65
+ const fragmentDefinitions = (form) => {
66
+ const definitions = [];
67
+ const inputs = form.querySelectorAll(
68
+ 'input[type="hidden"][name*="[fragments_attributes]"][name$="[identifier]"]'
69
+ );
70
+
71
+ for (const input of inputs) {
72
+ if (!fragmentIdentifierName.test(input.name)) continue;
73
+
74
+ definitions.push({
75
+ identifier: input.value,
76
+ prefix: input.name.slice(0, -"[identifier]".length),
77
+ });
78
+ }
79
+
80
+ return definitions;
81
+ };
82
+
83
+ const controlState = (control) => {
84
+ const type = (control.type || "").toLowerCase();
85
+
86
+ if (["button", "image", "reset", "submit", "file"].includes(type)) {
87
+ return null;
88
+ }
89
+
90
+ if (type === "checkbox" || type === "radio") {
91
+ return { kind: "checked", value: control.value, checked: control.checked };
92
+ }
93
+
94
+ if (control instanceof HTMLSelectElement && control.multiple) {
95
+ return {
96
+ kind: "selected",
97
+ values: Array.from(control.selectedOptions).map((option) => option.value),
98
+ };
99
+ }
100
+
101
+ return { kind: "value", value: control.value };
102
+ };
103
+
104
+ const addState = (entries, key, state) => {
105
+ let entry = entries.find((candidate) => candidate.key === key);
106
+ if (!entry) {
107
+ entry = { key, states: [] };
108
+ entries.push(entry);
109
+ }
110
+ entry.states.push(state);
111
+ };
112
+
113
+ const snapshot = (form) => {
114
+ const definitions = fragmentDefinitions(form);
115
+ const normal = [];
116
+ const fragments = [];
117
+ let filesSelected = false;
118
+
119
+ const fragmentEntry = (identifier) => {
120
+ let entry = fragments.find(
121
+ (candidate) => candidate.identifier === identifier
122
+ );
123
+ if (!entry) {
124
+ entry = { identifier, fields: [] };
125
+ fragments.push(entry);
126
+ }
127
+ return entry;
128
+ };
129
+
130
+ for (const control of Array.from(form.elements)) {
131
+ if (!control.name || ignoredNames.has(control.name)) continue;
132
+
133
+ const type = (control.type || "").toLowerCase();
134
+ if (type === "file") {
135
+ if (control.files && control.files.length > 0) filesSelected = true;
136
+ continue;
137
+ }
138
+
139
+ const state = controlState(control);
140
+ if (!state) continue;
141
+
142
+ const definition = definitions.find((candidate) =>
143
+ control.name.startsWith(`${candidate.prefix}[`)
144
+ );
145
+ if (definition) {
146
+ const suffix = control.name.slice(definition.prefix.length);
147
+ if (suffix === "[identifier]" || suffix === "[tag]") continue;
148
+ addState(fragmentEntry(definition.identifier).fields, suffix, state);
149
+ } else {
150
+ addState(normal, control.name, state);
151
+ }
152
+ }
153
+
154
+ const layout = form.querySelector("#fragments-toggle");
155
+ return {
156
+ version: VERSION,
157
+ savedAt: Date.now(),
158
+ layoutId: layout ? layout.value : null,
159
+ normal,
160
+ fragments,
161
+ filesSelected,
162
+ };
163
+ };
164
+
165
+ const comparableSnapshot = (value) => ({
166
+ version: value.version,
167
+ layoutId: value.layoutId,
168
+ normal: value.normal,
169
+ fragments: value.fragments,
170
+ filesSelected: value.filesSelected,
171
+ });
172
+
173
+ const snapshotsEqual = (left, right) =>
174
+ JSON.stringify(comparableSnapshot(left)) ===
175
+ JSON.stringify(comparableSnapshot(right));
176
+
177
+ const destinationState = (control) => ({
178
+ control,
179
+ ...controlState(control),
180
+ });
181
+
182
+ const correspondingState = (source, destinations, index) => {
183
+ if (source.kind === "checked") {
184
+ return destinations.find(
185
+ (destination) =>
186
+ destination.kind === "checked" && destination.value === source.value
187
+ );
188
+ }
189
+ return destinations[index];
190
+ };
191
+
192
+ const applyStates = (sourceStates, controls) => {
193
+ const destinations = controls
194
+ .map(destinationState)
195
+ .filter((state) => state.kind);
196
+
197
+ sourceStates.forEach((source, index) => {
198
+ const destination = correspondingState(source, destinations, index);
199
+ if (!destination || source.kind !== destination.kind) return;
200
+
201
+ if (source.kind === "checked") {
202
+ destination.control.checked = source.checked;
203
+ } else if (source.kind === "selected") {
204
+ for (const option of destination.control.options) {
205
+ option.selected = source.values.includes(option.value);
206
+ }
207
+ } else {
208
+ destination.control.value = source.value;
209
+ }
210
+ });
211
+ };
212
+
213
+ const applySnapshot = (form, value, { skipLayout = false } = {}) => {
214
+ const layout = form.querySelector("#fragments-toggle");
215
+
216
+ for (const entry of value.normal || []) {
217
+ if (skipLayout && layout && entry.key === layout.name) continue;
218
+ const controls = Array.from(form.elements).filter(
219
+ (control) => control.name === entry.key
220
+ );
221
+ applyStates(entry.states || [], controls);
222
+ }
223
+
224
+ const definitions = fragmentDefinitions(form);
225
+ for (const fragment of value.fragments || []) {
226
+ const definition = definitions.find(
227
+ (candidate) => candidate.identifier === fragment.identifier
228
+ );
229
+ if (!definition) continue;
230
+
231
+ for (const field of fragment.fields || []) {
232
+ const name = `${definition.prefix}${field.key}`;
233
+ const controls = Array.from(form.elements).filter(
234
+ (control) => control.name === name
235
+ );
236
+ applyStates(field.states || [], controls);
237
+ }
238
+ }
239
+
240
+ restoreEditors(form);
241
+ };
242
+
243
+ const save = (form) => {
244
+ if (!form.isConnected) return;
245
+ if (form.querySelector('[aria-busy="true"]')) {
246
+ schedule(form);
247
+ return;
248
+ }
249
+
250
+ syncEditors(form);
251
+ const value = snapshot(form);
252
+ const baseline = baselines.get(form);
253
+ const saved = baseline && snapshotsEqual(value, baseline)
254
+ ? removeStoredValue("localStorage", form.dataset.cmsDraftKey)
255
+ : writeStoredValue(
256
+ "localStorage",
257
+ form.dataset.cmsDraftKey,
258
+ JSON.stringify(value)
259
+ );
260
+ if (saved) changedForms.delete(form);
261
+ };
262
+
263
+ const schedule = (form) => {
264
+ if (suspendedForms.has(form)) return;
265
+ changedForms.add(form);
266
+ window.clearTimeout(timers.get(form));
267
+ timers.set(
268
+ form,
269
+ window.setTimeout(() => {
270
+ timers.delete(form);
271
+ save(form);
272
+ }, DEBOUNCE_DELAY)
273
+ );
274
+ };
275
+
276
+ const saveImmediately = (form) => {
277
+ window.clearTimeout(timers.get(form));
278
+ timers.delete(form);
279
+ changedForms.add(form);
280
+ save(form);
281
+ };
282
+
283
+ const flushChangedForms = () => {
284
+ for (const form of Array.from(changedForms)) saveImmediately(form);
285
+ };
286
+
287
+ const onSubmit = (event) => {
288
+ const form = event.target.closest("form[data-cms-draft-key]");
289
+ if (!form) return;
290
+
291
+ saveImmediately(form);
292
+ };
293
+
294
+ const acknowledgeSavedDraft = () => {
295
+ const meta = document.querySelector('meta[name="cms-saved-draft-key"]');
296
+ if (!meta) return;
297
+
298
+ if (meta.content) removeStoredValue("localStorage", meta.content);
299
+ meta.remove();
300
+ };
301
+
302
+ const closePrompt = (target = prompt) => {
303
+ if (!target || prompt !== target) return;
304
+
305
+ target.element.classList.remove("fade");
306
+ target.modal.hide();
307
+ target.modal.dispose();
308
+ target.element.remove();
309
+ for (const backdrop of document.querySelectorAll(".modal-backdrop")) {
310
+ backdrop.remove();
311
+ }
312
+ document.body.classList.remove("modal-open");
313
+ document.body.style.removeProperty("padding-right");
314
+ prompt = null;
315
+ };
316
+
317
+ const showPrompt = (form, value) => {
318
+ closePrompt();
319
+
320
+ const element = document.createElement("div");
321
+ element.className = "modal";
322
+ element.tabIndex = -1;
323
+ element.setAttribute("role", "dialog");
324
+ element.setAttribute("aria-modal", "true");
325
+ element.innerHTML = `
326
+ <div class="modal-dialog" role="document">
327
+ <div class="modal-content">
328
+ <div class="modal-header"><h5 class="modal-title"></h5></div>
329
+ <div class="modal-body">
330
+ <p data-cms-draft-message></p>
331
+ <p class="alert alert-warning mb-0" data-cms-draft-file-message></p>
332
+ </div>
333
+ <div class="modal-footer">
334
+ <button type="button" class="btn btn-outline-danger" data-cms-draft-discard></button>
335
+ <button type="button" class="btn btn-primary" data-cms-draft-restore></button>
336
+ </div>
337
+ </div>
338
+ </div>`;
339
+
340
+ element.querySelector(".modal-title").textContent =
341
+ form.dataset.cmsDraftTitle || "Unsaved draft found";
342
+ element.querySelector("[data-cms-draft-message]").textContent =
343
+ form.dataset.cmsDraftMessage ||
344
+ "A locally saved draft is available. Restore it or discard it?";
345
+
346
+ const fileMessage = element.querySelector(
347
+ "[data-cms-draft-file-message]"
348
+ );
349
+ fileMessage.textContent =
350
+ form.dataset.cmsDraftFilesMessage ||
351
+ "Selected files are not stored and must be selected again.";
352
+ fileMessage.hidden = !value.filesSelected;
353
+
354
+ const discardButton = element.querySelector("[data-cms-draft-discard]");
355
+ discardButton.textContent =
356
+ form.dataset.cmsDraftDiscard || "Discard draft";
357
+ discardButton.addEventListener("click", () => {
358
+ removeStoredValue("localStorage", form.dataset.cmsDraftKey);
359
+ closePrompt();
360
+ });
361
+
362
+ const restoreButton = element.querySelector("[data-cms-draft-restore]");
363
+ restoreButton.textContent =
364
+ form.dataset.cmsDraftRestore || "Restore draft";
365
+ restoreButton.addEventListener("click", async () => {
366
+ const activePrompt = prompt;
367
+ discardButton.disabled = true;
368
+ restoreButton.disabled = true;
369
+
370
+ const layout = form.querySelector("#fragments-toggle");
371
+ let layoutLoaded = true;
372
+ if (
373
+ layout &&
374
+ value.layoutId &&
375
+ value.layoutId !== layout.value &&
376
+ CMS.pageFragments &&
377
+ CMS.pageFragments.loadLayout
378
+ ) {
379
+ layoutLoaded = await CMS.pageFragments.loadLayout(value.layoutId, {
380
+ confirmDiscard: false,
381
+ preserveFragments: false,
382
+ });
383
+ }
384
+
385
+ if (!form.isConnected || prompt !== activePrompt) return;
386
+ applySnapshot(form, value, { skipLayout: !layoutLoaded });
387
+ closePrompt(activePrompt);
388
+ });
389
+
390
+ document.body.appendChild(element);
391
+ const modal = new Modal(element, { backdrop: "static", keyboard: false });
392
+ prompt = { element, modal };
393
+ modal.show();
394
+ };
395
+
396
+ const initializeForm = (form) => {
397
+ suspendedForms.add(form);
398
+ try {
399
+ syncEditors(form);
400
+ const current = snapshot(form);
401
+ baselines.set(
402
+ form,
403
+ form.dataset.cmsSaveFailed === "true" ? null : current
404
+ );
405
+
406
+ const value = parseStoredValue(
407
+ readStoredValue("localStorage", form.dataset.cmsDraftKey)
408
+ );
409
+ if (!value || value.version !== VERSION) return;
410
+
411
+ if (snapshotsEqual(value, current)) {
412
+ if (baselines.get(form)) {
413
+ removeStoredValue("localStorage", form.dataset.cmsDraftKey);
414
+ }
415
+ return;
416
+ }
417
+
418
+ showPrompt(form, value);
419
+ } finally {
420
+ suspendedForms.delete(form);
421
+ }
422
+ };
423
+
424
+ window.CMS.draftForms = {
425
+ init() {
426
+ forms = Array.from(document.querySelectorAll("form[data-cms-draft-key]"));
427
+ timers = new Map();
428
+ changedForms = new Set();
429
+ suspendedForms = new Set();
430
+ formHandlers = new Map();
431
+ baselines = new WeakMap();
432
+ acknowledgeSavedDraft();
433
+
434
+ for (const form of forms) {
435
+ const scheduleSave = () => schedule(form);
436
+ formHandlers.set(form, scheduleSave);
437
+ form.addEventListener("input", scheduleSave);
438
+ form.addEventListener("change", scheduleSave);
439
+ form.addEventListener("cms:fragments-changed", scheduleSave);
440
+ initializeForm(form);
441
+ }
442
+ document.addEventListener("submit", onSubmit, true);
443
+ window.addEventListener("beforeunload", flushChangedForms);
444
+ window.addEventListener("pagehide", flushChangedForms);
445
+ },
446
+
447
+ dispose() {
448
+ document.removeEventListener("submit", onSubmit, true);
449
+ window.removeEventListener("beforeunload", flushChangedForms);
450
+ window.removeEventListener("pagehide", flushChangedForms);
451
+ for (const [form, scheduleSave] of formHandlers) {
452
+ form.removeEventListener("input", scheduleSave);
453
+ form.removeEventListener("change", scheduleSave);
454
+ form.removeEventListener("cms:fragments-changed", scheduleSave);
455
+ }
456
+ flushChangedForms();
457
+ for (const timer of timers.values()) window.clearTimeout(timer);
458
+ closePrompt();
459
+
460
+ forms = [];
461
+ timers = new Map();
462
+ changedForms = new Set();
463
+ suspendedForms = new Set();
464
+ formHandlers = new Map();
465
+ baselines = new WeakMap();
466
+ },
467
+ };
468
+ })();