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
@@ -1,22 +1,393 @@
1
- window.CMS.pageFragments = () => {
2
- const toggle = document.querySelector('select#fragments-toggle');
3
- if (toggle === null) return;
4
- const url = new URL(toggle.dataset.url, document.location.href);
5
- toggle.addEventListener('change', () => {
6
- url.searchParams.set('layout_id', toggle.value);
7
- fetch(url, {credentials: 'same-origin'}).then((resp) => resp.text()).then((html) => {
8
- const container = document.querySelector('#form-fragments-container');
9
- container.innerHTML = html;
10
- // TODO: Only dispose of the widgets that were within the fragment.
11
- CMS.wysiwyg.dispose();
12
- CMS.timepicker.dispose();
13
- CMS.codemirror.dispose();
14
-
15
- CMS.fileLinks(container);
16
- // TODO: Container should also be passed here once the TODO above is addressed.
17
- CMS.wysiwyg.init();
18
- CMS.timepicker.init();
19
- CMS.codemirror.init();
20
- });
21
- });
22
- };
1
+ (() => {
2
+ const fragmentName = /\[fragments_attributes\]\[\d+\]\[identifier\]$/;
3
+
4
+ let toggle = null;
5
+ let container = null;
6
+ let currentLayoutId = null;
7
+ let baselineFragments = new Map();
8
+ let onLayoutChange = null;
9
+ let requestId = 0;
10
+ let abortController = null;
11
+
12
+ const fieldState = (control) => {
13
+ const type = (control.type || "").toLowerCase();
14
+
15
+ if (type === "file") {
16
+ return {
17
+ control,
18
+ kind: "file",
19
+ files: Array.from(control.files),
20
+ };
21
+ }
22
+
23
+ if (type === "checkbox" || type === "radio") {
24
+ return {
25
+ control,
26
+ kind: "checked",
27
+ value: control.value,
28
+ checked: control.checked,
29
+ };
30
+ }
31
+
32
+ if (control instanceof HTMLSelectElement && control.multiple) {
33
+ return {
34
+ control,
35
+ kind: "selected",
36
+ values: Array.from(control.selectedOptions).map(
37
+ (option) => option.value
38
+ ),
39
+ };
40
+ }
41
+
42
+ return { control, kind: "value", value: control.value };
43
+ };
44
+
45
+ const fragmentStates = (root) => {
46
+ const fragments = new Map();
47
+ const identifierInputs = root.querySelectorAll(
48
+ 'input[type="hidden"][name*="[fragments_attributes]"][name$="[identifier]"]'
49
+ );
50
+
51
+ for (const identifierInput of identifierInputs) {
52
+ if (!fragmentName.test(identifierInput.name)) continue;
53
+
54
+ const prefix = identifierInput.name.slice(0, -"[identifier]".length);
55
+ const fields = new Map();
56
+ const controls = Array.from(root.querySelectorAll("[name]")).filter(
57
+ (control) =>
58
+ control.name.startsWith(`${prefix}[`) &&
59
+ control.type !== "hidden" &&
60
+ !control.disabled
61
+ );
62
+
63
+ for (const control of controls) {
64
+ const key = control.name.slice(prefix.length);
65
+ if (!fields.has(key)) fields.set(key, []);
66
+ fields.get(key).push(fieldState(control));
67
+ }
68
+
69
+ const wrapper = identifierInput.closest(".form-group");
70
+ fragments.set(identifierInput.value, {
71
+ fields,
72
+ hasAttachments:
73
+ wrapper && wrapper.querySelector(".fragment-attachment") !== null,
74
+ });
75
+ }
76
+
77
+ return fragments;
78
+ };
79
+
80
+ const meaningful = (state) => {
81
+ if (state.kind === "file") return state.files.length > 0;
82
+ if (state.kind === "checked") return state.checked;
83
+ if (state.kind === "selected") return state.values.length > 0;
84
+ return state.value !== "";
85
+ };
86
+
87
+ const compatible = (source, destination) => {
88
+ if (!destination || source.kind === "file") return false;
89
+ if (source.kind !== destination.kind) return false;
90
+ if (source.kind === "checked") return source.value === destination.value;
91
+ return true;
92
+ };
93
+
94
+ const correspondingState = (source, states, index) =>
95
+ source.kind === "checked"
96
+ ? states.find(
97
+ (state) =>
98
+ state.kind === "checked" && state.value === source.value
99
+ )
100
+ : states[index];
101
+
102
+ const statesEqual = (source, baseline) => {
103
+ if (source.kind === "file") {
104
+ if (!baseline || baseline.kind !== "file") return false;
105
+ return (
106
+ source.files.length === baseline.files.length &&
107
+ source.files.every(
108
+ (file, index) =>
109
+ file.name === baseline.files[index].name &&
110
+ file.size === baseline.files[index].size &&
111
+ file.lastModified === baseline.files[index].lastModified
112
+ )
113
+ );
114
+ }
115
+
116
+ if (!compatible(source, baseline)) return false;
117
+ if (source.kind === "checked") return source.checked === baseline.checked;
118
+ if (source.kind === "selected") {
119
+ return (
120
+ source.values.length === baseline.values.length &&
121
+ source.values.every((value, index) => value === baseline.values[index])
122
+ );
123
+ }
124
+ return source.value === baseline.value;
125
+ };
126
+
127
+ const stateChanged = (source, baselineStates, index) =>
128
+ !statesEqual(source, correspondingState(source, baselineStates, index));
129
+
130
+ const fragmentHasContent = (fragment) =>
131
+ fragment.hasAttachments ||
132
+ Array.from(fragment.fields.values()).some((states) =>
133
+ states.some(meaningful)
134
+ );
135
+
136
+ const fragmentChanged = (sourceFragment, baselineFragment) => {
137
+ if (!baselineFragment) return fragmentHasContent(sourceFragment);
138
+
139
+ for (const [key, sourceStates] of sourceFragment.fields) {
140
+ const baselineStates = baselineFragment.fields.get(key) || [];
141
+ if (
142
+ sourceStates.some((source, index) =>
143
+ stateChanged(source, baselineStates, index)
144
+ )
145
+ ) {
146
+ return true;
147
+ }
148
+ }
149
+
150
+ return false;
151
+ };
152
+
153
+ const wouldDiscardContent = (
154
+ sourceFragments,
155
+ destinationFragments,
156
+ baselineFragments
157
+ ) => {
158
+ for (const [identifier, sourceFragment] of sourceFragments) {
159
+ const destinationFragment = destinationFragments.get(identifier);
160
+ const baselineFragment = baselineFragments.get(identifier);
161
+ if (!destinationFragment) {
162
+ if (
163
+ fragmentHasContent(sourceFragment) ||
164
+ fragmentChanged(sourceFragment, baselineFragment)
165
+ ) {
166
+ return true;
167
+ }
168
+ continue;
169
+ }
170
+
171
+ if (
172
+ sourceFragment.hasAttachments &&
173
+ !destinationFragment.hasAttachments
174
+ ) {
175
+ return true;
176
+ }
177
+
178
+ for (const [key, sourceStates] of sourceFragment.fields) {
179
+ const destinationStates = destinationFragment.fields.get(key) || [];
180
+ const baselineStates = baselineFragment
181
+ ? baselineFragment.fields.get(key) || []
182
+ : [];
183
+
184
+ for (let index = 0; index < sourceStates.length; index += 1) {
185
+ const source = sourceStates[index];
186
+ const destination = correspondingState(
187
+ source,
188
+ destinationStates,
189
+ index
190
+ );
191
+ if (compatible(source, destination)) continue;
192
+
193
+ if (
194
+ meaningful(source) ||
195
+ stateChanged(source, baselineStates, index)
196
+ ) {
197
+ return true;
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ return false;
204
+ };
205
+
206
+ const restoreFragments = (sourceFragments, root) => {
207
+ const destinationFragments = fragmentStates(root);
208
+
209
+ for (const [identifier, sourceFragment] of sourceFragments) {
210
+ const destinationFragment = destinationFragments.get(identifier);
211
+ if (!destinationFragment) continue;
212
+
213
+ for (const [key, sourceStates] of sourceFragment.fields) {
214
+ const destinationStates = destinationFragment.fields.get(key) || [];
215
+
216
+ for (let index = 0; index < sourceStates.length; index += 1) {
217
+ const source = sourceStates[index];
218
+ const destination = correspondingState(
219
+ source,
220
+ destinationStates,
221
+ index
222
+ );
223
+
224
+ if (!compatible(source, destination)) continue;
225
+
226
+ if (source.kind === "checked") {
227
+ destination.control.checked = source.checked;
228
+ } else if (source.kind === "selected") {
229
+ for (const option of destination.control.options) {
230
+ option.selected = source.values.includes(option.value);
231
+ }
232
+ } else {
233
+ destination.control.value = source.value;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ };
239
+
240
+ const responseFragment = (html) => {
241
+ const template = document.createElement("template");
242
+ template.innerHTML = html.trim();
243
+ return template.content;
244
+ };
245
+
246
+ const statusMessage = (container, message, type) => {
247
+ const current = container.parentElement
248
+ ? container.parentElement.querySelector("[data-cms-fragments-status]")
249
+ : null;
250
+ if (current) current.remove();
251
+ if (!message) return;
252
+
253
+ const status = document.createElement("div");
254
+ status.className = `alert alert-${type}`;
255
+ status.dataset.cmsFragmentsStatus = "true";
256
+ status.setAttribute("role", type === "danger" ? "alert" : "status");
257
+ status.textContent = message;
258
+ container.insertAdjacentElement("beforebegin", status);
259
+ };
260
+
261
+ const replaceFragments = (container, html, sourceFragments) => {
262
+ // TODO: Only dispose of the widgets that were within the fragment.
263
+ CMS.wysiwyg.dispose();
264
+ CMS.timepicker.dispose();
265
+ CMS.codemirror.dispose();
266
+
267
+ container.innerHTML = html;
268
+ restoreFragments(sourceFragments, container);
269
+
270
+ CMS.fileLinks(container);
271
+ // TODO: Container should also be passed here once the TODO above is addressed.
272
+ CMS.wysiwyg.init();
273
+ CMS.timepicker.init();
274
+ CMS.codemirror.init();
275
+ };
276
+
277
+ const notifyChanged = () => {
278
+ if (!toggle) return;
279
+ toggle.dispatchEvent(
280
+ new CustomEvent("cms:fragments-changed", { bubbles: true })
281
+ );
282
+ };
283
+
284
+ const loadLayout = async (
285
+ requestedLayoutId,
286
+ { confirmDiscard = true, preserveFragments = true } = {}
287
+ ) => {
288
+ if (!toggle || !container) return false;
289
+ if (requestedLayoutId === currentLayoutId) return true;
290
+
291
+ CMS.codemirror.sync(container);
292
+ CMS.wysiwyg.sync(container);
293
+ const sourceFragments = fragmentStates(container);
294
+ const url = new URL(toggle.dataset.url, document.location.href);
295
+ url.searchParams.set("layout_id", requestedLayoutId);
296
+
297
+ const currentRequestId = ++requestId;
298
+ if (abortController) abortController.abort();
299
+ abortController = new AbortController();
300
+
301
+ toggle.value = requestedLayoutId;
302
+ toggle.disabled = true;
303
+ container.setAttribute("aria-busy", "true");
304
+ statusMessage(container, toggle.dataset.loadingMessage, "info");
305
+
306
+ try {
307
+ const response = await fetch(url, {
308
+ credentials: "same-origin",
309
+ signal: abortController.signal,
310
+ });
311
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
312
+
313
+ const html = await response.text();
314
+ if (currentRequestId !== requestId || !container) return false;
315
+
316
+ const destinationFragments = fragmentStates(responseFragment(html));
317
+ statusMessage(container, "", "info");
318
+
319
+ if (
320
+ confirmDiscard &&
321
+ wouldDiscardContent(
322
+ sourceFragments,
323
+ destinationFragments,
324
+ baselineFragments
325
+ ) &&
326
+ !window.confirm(toggle.dataset.discardMessage)
327
+ ) {
328
+ toggle.value = currentLayoutId;
329
+ return false;
330
+ }
331
+
332
+ replaceFragments(
333
+ container,
334
+ html,
335
+ preserveFragments ? sourceFragments : new Map()
336
+ );
337
+ currentLayoutId = requestedLayoutId;
338
+ baselineFragments = destinationFragments;
339
+ return true;
340
+ } catch (error) {
341
+ if (error.name === "AbortError") return false;
342
+
343
+ toggle.value = currentLayoutId;
344
+ statusMessage(container, toggle.dataset.errorMessage, "danger");
345
+ console.error("Unable to load layout fragments", error);
346
+ return false;
347
+ } finally {
348
+ if (currentRequestId === requestId && toggle && container) {
349
+ toggle.disabled = false;
350
+ container.removeAttribute("aria-busy");
351
+ abortController = null;
352
+ notifyChanged();
353
+ }
354
+ }
355
+ };
356
+
357
+ const dispose = () => {
358
+ requestId += 1;
359
+ if (abortController) abortController.abort();
360
+ if (toggle) {
361
+ toggle.value = currentLayoutId;
362
+ toggle.disabled = false;
363
+ if (onLayoutChange) toggle.removeEventListener("change", onLayoutChange);
364
+ }
365
+ if (container) {
366
+ container.removeAttribute("aria-busy");
367
+ statusMessage(container, "", "info");
368
+ }
369
+
370
+ toggle = null;
371
+ container = null;
372
+ currentLayoutId = null;
373
+ baselineFragments = new Map();
374
+ onLayoutChange = null;
375
+ abortController = null;
376
+ };
377
+
378
+ window.CMS.pageFragments = {
379
+ init() {
380
+ dispose();
381
+ toggle = document.querySelector("select#fragments-toggle");
382
+ container = document.querySelector("#form-fragments-container");
383
+ if (!toggle || !container) return;
384
+
385
+ currentLayoutId = toggle.value;
386
+ baselineFragments = fragmentStates(container);
387
+ onLayoutChange = () => loadLayout(toggle.value);
388
+ toggle.addEventListener("change", onLayoutChange);
389
+ },
390
+ loadLayout,
391
+ dispose,
392
+ };
393
+ })();
@@ -66,6 +66,10 @@ import "../../vendor/redactor/video";
66
66
  removeNewLines: false,
67
67
  deniedTags: [],
68
68
  replaceDivs: false,
69
+ changeCallback() {
70
+ const textarea = this.core.getTextarea()[0];
71
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
72
+ },
69
73
  };
70
74
  };
71
75
 
@@ -81,6 +85,23 @@ import "../../vendor/redactor/video";
81
85
  redactorInstances.push(new jQuery.Redactor(textarea, redactorOptions));
82
86
  }
83
87
  },
88
+ sync(root = document) {
89
+ for (const redactor of redactorInstances) {
90
+ const textarea = redactor.core.getTextarea()[0];
91
+ if (root.contains(textarea) && redactor.opts.visual) {
92
+ redactor.code.startSync();
93
+ }
94
+ }
95
+ },
96
+ restore(root = document) {
97
+ for (const redactor of redactorInstances) {
98
+ const textarea = redactor.core.getTextarea()[0];
99
+ if (!root.contains(textarea)) continue;
100
+
101
+ redactor.code.set(textarea.value);
102
+ redactor.code.startSync();
103
+ }
104
+ },
84
105
  dispose() {
85
106
  for (const redactor of redactorInstances) {
86
107
  redactor.core.destroy();
@@ -24,42 +24,60 @@ if (!RedactorPlugins) var RedactorPlugins = {};
24
24
  var $box = $(
25
25
  '<div id="redactor-file-manager-box" style="overflow: auto; height: 300px;" class="redactor-tab redactor-tab2">'
26
26
  ).hide();
27
+ var $search = $(
28
+ '<input type="search" class="form-control" placeholder="Search files" style="margin-bottom: 10px;">'
29
+ );
30
+ var $results = $("<div>");
31
+ $box.append($search, $results);
27
32
  $modal.append($box);
28
33
 
29
- $.ajax({
30
- dataType: "json",
31
- cache: false,
32
- url: this.opts.fileManagerJson,
33
- success: $.proxy(function (data) {
34
- var ul = $('<ul id="redactor-modal-list">');
35
- $.each(
36
- data,
37
- $.proxy(function (key, val) {
38
- var a = $(
39
- '<a href="#" title="' +
40
- val.title +
41
- '" rel="' +
42
- val.link +
43
- '" class="redactor-file-manager-link">' +
44
- val.title +
45
- ' <span style="font-size: 11px; color: #888;">' +
46
- val.name +
47
- '</span> <span style="position: absolute; right: 10px; font-size: 11px; color: #888;">(' +
48
- val.size +
49
- ")</span></a>"
50
- );
51
- var li = $("<li />");
34
+ var request;
35
+ var timer;
36
+ var loadFiles = $.proxy(function () {
37
+ if (request) request.abort();
38
+ request = $.ajax({
39
+ dataType: "json",
40
+ cache: false,
41
+ data: { q: $search.val() },
42
+ url: this.opts.fileManagerJson,
43
+ success: $.proxy(function (data) {
44
+ var ul = $('<ul id="redactor-modal-list">');
45
+ $results.empty();
46
+ $.each(
47
+ data,
48
+ $.proxy(function (key, val) {
49
+ var a = $(
50
+ '<a href="#" title="' +
51
+ val.title +
52
+ '" rel="' +
53
+ val.link +
54
+ '" class="redactor-file-manager-link">' +
55
+ val.title +
56
+ ' <span style="font-size: 11px; color: #888;">' +
57
+ val.name +
58
+ '</span> <span style="position: absolute; right: 10px; font-size: 11px; color: #888;">(' +
59
+ val.size +
60
+ ")</span></a>"
61
+ );
62
+ var li = $("<li />");
63
+
64
+ a.on("click", $.proxy(this.filemanager.insert, this));
52
65
 
53
- a.on("click", $.proxy(this.filemanager.insert, this));
66
+ li.append(a);
67
+ ul.append(li);
68
+ }, this)
69
+ );
54
70
 
55
- li.append(a);
56
- ul.append(li);
57
- }, this)
58
- );
71
+ $results.append(ul);
72
+ }, this),
73
+ });
74
+ }, this);
59
75
 
60
- $("#redactor-file-manager-box").append(ul);
61
- }, this),
76
+ $search.on("input", function () {
77
+ clearTimeout(timer);
78
+ timer = setTimeout(loadFiles, 250);
62
79
  });
80
+ loadFiles();
63
81
  },
64
82
  insert: function (e) {
65
83
  e.preventDefault();
@@ -24,35 +24,53 @@ if (!RedactorPlugins) var RedactorPlugins = {};
24
24
  var $box = $(
25
25
  '<div id="redactor-image-manager-box" style="overflow: auto; height: 300px;" class="redactor-tab redactor-tab2">'
26
26
  ).hide();
27
+ var $search = $(
28
+ '<input type="search" class="form-control" placeholder="Search images" style="margin-bottom: 10px;">'
29
+ );
30
+ var $results = $("<div>");
31
+ $box.append($search, $results);
27
32
  $modal.append($box);
28
33
 
29
- $.ajax({
30
- dataType: "json",
31
- cache: false,
32
- url: this.opts.imageManagerJson,
33
- success: $.proxy(function (data) {
34
- $.each(
35
- data,
36
- $.proxy(function (key, val) {
37
- // title
38
- var thumbtitle = "";
39
- if (typeof val.title !== "undefined") thumbtitle = val.title;
34
+ var request;
35
+ var timer;
36
+ var loadImages = $.proxy(function () {
37
+ if (request) request.abort();
38
+ request = $.ajax({
39
+ dataType: "json",
40
+ cache: false,
41
+ data: { q: $search.val() },
42
+ url: this.opts.imageManagerJson,
43
+ success: $.proxy(function (data) {
44
+ $results.empty();
45
+ $.each(
46
+ data,
47
+ $.proxy(function (key, val) {
48
+ // title
49
+ var thumbtitle = "";
50
+ if (typeof val.title !== "undefined") thumbtitle = val.title;
51
+
52
+ var img = $(
53
+ '<img src="' +
54
+ val.thumb +
55
+ '" rel="' +
56
+ val.image +
57
+ '" title="' +
58
+ thumbtitle +
59
+ '" style="width: 100px; height: 75px; cursor: pointer;" />'
60
+ );
61
+ $results.append(img);
62
+ $(img).click($.proxy(this.imagemanager.insert, this));
63
+ }, this)
64
+ );
65
+ }, this),
66
+ });
67
+ }, this);
40
68
 
41
- var img = $(
42
- '<img src="' +
43
- val.thumb +
44
- '" rel="' +
45
- val.image +
46
- '" title="' +
47
- thumbtitle +
48
- '" style="width: 100px; height: 75px; cursor: pointer;" />'
49
- );
50
- $("#redactor-image-manager-box").append(img);
51
- $(img).click($.proxy(this.imagemanager.insert, this));
52
- }, this)
53
- );
54
- }, this),
69
+ $search.on("input", function () {
70
+ clearTimeout(timer);
71
+ timer = setTimeout(loadImages, 250);
55
72
  });
73
+ loadImages();
56
74
  },
57
75
  insert: function (e) {
58
76
  this.image.insert(
@@ -217,6 +217,29 @@ body#comfy
217
217
  .label
218
218
  padding: 15px 0
219
219
  font-weight: bold
220
+ .revision-summary
221
+ margin-bottom: 1rem
222
+ padding: 1rem
223
+ background-color: $white
224
+ border: 1px solid $gray-300
225
+ .revision-legend
226
+ display: flex
227
+ flex-wrap: wrap
228
+ gap: 1rem
229
+ font-size: 0.875rem
230
+ .revision-legend-item
231
+ display: inline-flex
232
+ align-items: center
233
+ .revision-legend-swatch
234
+ display: inline-block
235
+ width: 1.25rem
236
+ height: 1rem
237
+ margin-right: 0.35rem
238
+ border: 1px solid $gray-400
239
+ &.added
240
+ background-color: #c6ffc6
241
+ &.removed
242
+ background-color: #ffc6c6
220
243
  .revision
221
244
  margin-bottom: 5px
222
245
  .content
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Comfy::Admin::BaseController < ComfortableMediaSurfer.config.admin_base_controller.to_s.constantize
3
+ class Comfy::Admin::BaseController < ComfyMiddleSeat.config.admin_base_controller.to_s.constantize
4
4
  include Comfy::Paginate
5
5
 
6
6
  # Authentication module must have `authenticate` method
7
- include ComfortableMediaSurfer.config.admin_auth.to_s.constantize
7
+ include ComfyMiddleSeat.config.admin_auth.to_s.constantize
8
8
 
9
9
  # Authorization module must have `authorize` method
10
- include ComfortableMediaSurfer.config.admin_authorization.to_s.constantize
10
+ include ComfyMiddleSeat.config.admin_authorization.to_s.constantize
11
11
 
12
12
  helper Comfy::Admin::CmsHelper
13
13
  helper Comfy::CmsHelper