active_canvas 0.0.1

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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +318 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/active_canvas/editor/ai_panel.js +1607 -0
  6. data/app/assets/javascripts/active_canvas/editor/asset_manager.js +498 -0
  7. data/app/assets/javascripts/active_canvas/editor/blocks.js +1083 -0
  8. data/app/assets/javascripts/active_canvas/editor/code_panel.js +572 -0
  9. data/app/assets/javascripts/active_canvas/editor/component_toolbar.js +394 -0
  10. data/app/assets/javascripts/active_canvas/editor/panels.js +460 -0
  11. data/app/assets/javascripts/active_canvas/editor/utils.js +56 -0
  12. data/app/assets/javascripts/active_canvas/editor.js +295 -0
  13. data/app/assets/stylesheets/active_canvas/application.css +15 -0
  14. data/app/assets/stylesheets/active_canvas/editor.css +2929 -0
  15. data/app/controllers/active_canvas/admin/ai_controller.rb +181 -0
  16. data/app/controllers/active_canvas/admin/application_controller.rb +56 -0
  17. data/app/controllers/active_canvas/admin/media_controller.rb +61 -0
  18. data/app/controllers/active_canvas/admin/page_types_controller.rb +57 -0
  19. data/app/controllers/active_canvas/admin/page_versions_controller.rb +23 -0
  20. data/app/controllers/active_canvas/admin/pages_controller.rb +133 -0
  21. data/app/controllers/active_canvas/admin/partials_controller.rb +88 -0
  22. data/app/controllers/active_canvas/admin/settings_controller.rb +256 -0
  23. data/app/controllers/active_canvas/application_controller.rb +20 -0
  24. data/app/controllers/active_canvas/pages_controller.rb +18 -0
  25. data/app/controllers/concerns/active_canvas/current_user.rb +12 -0
  26. data/app/controllers/concerns/active_canvas/rate_limitable.rb +75 -0
  27. data/app/controllers/concerns/active_canvas/tailwind_compilation.rb +39 -0
  28. data/app/helpers/active_canvas/application_helper.rb +4 -0
  29. data/app/jobs/active_canvas/application_job.rb +4 -0
  30. data/app/jobs/active_canvas/compile_tailwind_job.rb +64 -0
  31. data/app/mailers/active_canvas/application_mailer.rb +6 -0
  32. data/app/models/active_canvas/ai_model.rb +136 -0
  33. data/app/models/active_canvas/application_record.rb +5 -0
  34. data/app/models/active_canvas/media.rb +141 -0
  35. data/app/models/active_canvas/page.rb +85 -0
  36. data/app/models/active_canvas/page_type.rb +22 -0
  37. data/app/models/active_canvas/page_version.rb +80 -0
  38. data/app/models/active_canvas/partial.rb +73 -0
  39. data/app/models/active_canvas/setting.rb +292 -0
  40. data/app/services/active_canvas/ai_configuration.rb +40 -0
  41. data/app/services/active_canvas/ai_models.rb +128 -0
  42. data/app/services/active_canvas/ai_service.rb +289 -0
  43. data/app/services/active_canvas/content_sanitizer.rb +112 -0
  44. data/app/services/active_canvas/tailwind_compiler.rb +156 -0
  45. data/app/views/active_canvas/admin/media/index.html.erb +401 -0
  46. data/app/views/active_canvas/admin/media/show.html.erb +297 -0
  47. data/app/views/active_canvas/admin/page_types/_form.html.erb +25 -0
  48. data/app/views/active_canvas/admin/page_types/edit.html.erb +13 -0
  49. data/app/views/active_canvas/admin/page_types/index.html.erb +29 -0
  50. data/app/views/active_canvas/admin/page_types/new.html.erb +9 -0
  51. data/app/views/active_canvas/admin/page_types/show.html.erb +18 -0
  52. data/app/views/active_canvas/admin/page_versions/show.html.erb +469 -0
  53. data/app/views/active_canvas/admin/pages/_form.html.erb +62 -0
  54. data/app/views/active_canvas/admin/pages/content.html.erb +139 -0
  55. data/app/views/active_canvas/admin/pages/edit.html.erb +335 -0
  56. data/app/views/active_canvas/admin/pages/editor.html.erb +710 -0
  57. data/app/views/active_canvas/admin/pages/index.html.erb +149 -0
  58. data/app/views/active_canvas/admin/pages/new.html.erb +19 -0
  59. data/app/views/active_canvas/admin/pages/show.html.erb +258 -0
  60. data/app/views/active_canvas/admin/pages/versions.html.erb +333 -0
  61. data/app/views/active_canvas/admin/partials/edit.html.erb +182 -0
  62. data/app/views/active_canvas/admin/partials/editor.html.erb +703 -0
  63. data/app/views/active_canvas/admin/partials/index.html.erb +131 -0
  64. data/app/views/active_canvas/admin/settings/show.html.erb +1864 -0
  65. data/app/views/active_canvas/pages/no_homepage.html.erb +45 -0
  66. data/app/views/active_canvas/pages/show.html.erb +113 -0
  67. data/app/views/layouts/active_canvas/admin/application.html.erb +960 -0
  68. data/app/views/layouts/active_canvas/admin/editor.html.erb +826 -0
  69. data/app/views/layouts/active_canvas/application.html.erb +55 -0
  70. data/config/routes.rb +48 -0
  71. data/db/migrate/20260202000001_create_active_canvas_tables.rb +113 -0
  72. data/db/migrate/20260202000002_create_active_canvas_ai_models.rb +26 -0
  73. data/lib/active_canvas/configuration.rb +232 -0
  74. data/lib/active_canvas/engine.rb +44 -0
  75. data/lib/active_canvas/version.rb +3 -0
  76. data/lib/active_canvas.rb +26 -0
  77. data/lib/generators/active_canvas/install/install_generator.rb +263 -0
  78. data/lib/generators/active_canvas/install/templates/initializer.rb.tt +163 -0
  79. data/lib/tasks/active_canvas_tasks.rake +69 -0
  80. metadata +150 -0
@@ -0,0 +1,460 @@
1
+ /**
2
+ * ActiveCanvas Editor - Panel Controls, Device Switching, Save
3
+ */
4
+
5
+ (function() {
6
+ 'use strict';
7
+
8
+ window.ActiveCanvasEditor = window.ActiveCanvasEditor || {};
9
+
10
+ /**
11
+ * Setup panel controls (toggle left/right/AI panels, tabs)
12
+ * @param {Object} editor - GrapeJS editor instance
13
+ */
14
+ function setupPanelControls(editor) {
15
+ const panelLeft = document.getElementById('panel-left');
16
+ const panelRight = document.getElementById('panel-right');
17
+ const panelAi = document.getElementById('panel-ai');
18
+ const btnToggleLeft = document.getElementById('btn-toggle-left');
19
+ const btnToggleRight = document.getElementById('btn-toggle-right');
20
+ const btnToggleAi = document.getElementById('btn-toggle-ai');
21
+ const btnCloseAi = document.getElementById('btn-close-ai');
22
+
23
+ function refreshEditor() {
24
+ setTimeout(() => {
25
+ editor.refresh();
26
+ }, 250);
27
+ }
28
+
29
+ // Toggle left panel (blocks/assets/layers)
30
+ if (btnToggleLeft && panelLeft) {
31
+ btnToggleLeft.addEventListener('click', function() {
32
+ const isOpening = panelLeft.classList.contains('collapsed');
33
+
34
+ if (isOpening && panelAi) {
35
+ // Close AI panel when opening left panel
36
+ panelAi.classList.add('collapsed');
37
+ if (btnToggleAi) btnToggleAi.classList.remove('active');
38
+ }
39
+
40
+ panelLeft.classList.toggle('collapsed');
41
+ this.classList.toggle('active', !panelLeft.classList.contains('collapsed'));
42
+ refreshEditor();
43
+ });
44
+ }
45
+
46
+ // Toggle AI panel
47
+ if (btnToggleAi && panelAi) {
48
+ btnToggleAi.addEventListener('click', function() {
49
+ const isOpening = panelAi.classList.contains('collapsed');
50
+
51
+ if (isOpening && panelLeft) {
52
+ // Close left panel when opening AI panel
53
+ panelLeft.classList.add('collapsed');
54
+ if (btnToggleLeft) btnToggleLeft.classList.remove('active');
55
+ }
56
+
57
+ panelAi.classList.toggle('collapsed');
58
+ this.classList.toggle('active', !panelAi.classList.contains('collapsed'));
59
+ refreshEditor();
60
+ });
61
+ }
62
+
63
+ // Close AI panel button
64
+ if (btnCloseAi && panelAi) {
65
+ btnCloseAi.addEventListener('click', function() {
66
+ panelAi.classList.add('collapsed');
67
+ if (btnToggleAi) btnToggleAi.classList.remove('active');
68
+ refreshEditor();
69
+ });
70
+ }
71
+
72
+ // Toggle right panel
73
+ if (btnToggleRight && panelRight) {
74
+ btnToggleRight.addEventListener('click', function() {
75
+ panelRight.classList.toggle('collapsed');
76
+ this.classList.toggle('active', !panelRight.classList.contains('collapsed'));
77
+ refreshEditor();
78
+ });
79
+ }
80
+
81
+ // Panel tab switching
82
+ document.querySelectorAll('.panel-tab').forEach(tab => {
83
+ tab.addEventListener('click', function() {
84
+ const panel = this.dataset.panel;
85
+ const parent = this.closest('.editor-panel-left, .editor-panel-right');
86
+
87
+ // Update active tab
88
+ parent.querySelectorAll('.panel-tab').forEach(t => t.classList.remove('active'));
89
+ this.classList.add('active');
90
+
91
+ // Show/hide content
92
+ if (parent.classList.contains('editor-panel-left')) {
93
+ const blocksContainer = document.getElementById('blocks-container');
94
+ const assetsContainer = document.getElementById('assets-container');
95
+ const layersContainer = document.getElementById('layers-container');
96
+
97
+ if (blocksContainer) blocksContainer.style.display = panel === 'blocks' ? 'block' : 'none';
98
+ if (assetsContainer) assetsContainer.style.display = panel === 'assets' ? 'block' : 'none';
99
+ if (layersContainer) layersContainer.style.display = panel === 'layers' ? 'block' : 'none';
100
+ } else {
101
+ const stylesContainer = document.getElementById('styles-container');
102
+ const traitsContainer = document.getElementById('traits-container');
103
+
104
+ if (stylesContainer) stylesContainer.style.display = panel === 'styles' ? 'block' : 'none';
105
+ if (traitsContainer) traitsContainer.style.display = panel === 'settings' ? 'block' : 'none';
106
+ }
107
+ });
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Setup device switching for responsive preview
113
+ * @param {Object} editor - GrapeJS editor instance
114
+ */
115
+ function setupDeviceSwitching(editor) {
116
+ document.querySelectorAll('.device-btn').forEach(btn => {
117
+ btn.addEventListener('click', function() {
118
+ const device = this.dataset.device;
119
+ document.querySelectorAll('.device-btn').forEach(b => b.classList.remove('active'));
120
+ this.classList.add('active');
121
+
122
+ const deviceMap = {
123
+ 'desktop': 'Desktop',
124
+ 'tablet': 'Tablet',
125
+ 'mobile': 'Mobile'
126
+ };
127
+
128
+ // Set the device in GrapeJS
129
+ editor.setDevice(deviceMap[device]);
130
+
131
+ // Refresh the canvas to ensure proper rendering
132
+ setTimeout(() => {
133
+ editor.refresh();
134
+ }, 100);
135
+ });
136
+ });
137
+
138
+ // Set initial device to Desktop on load
139
+ editor.on('load', () => {
140
+ editor.setDevice('Desktop');
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Setup undo/redo buttons
146
+ * @param {Object} editor - GrapeJS editor instance
147
+ */
148
+ function setupUndoRedo(editor) {
149
+ const undoBtn = document.getElementById('btn-undo');
150
+ const redoBtn = document.getElementById('btn-redo');
151
+
152
+ if (undoBtn) {
153
+ undoBtn.addEventListener('click', () => {
154
+ editor.UndoManager.undo();
155
+ });
156
+ }
157
+
158
+ if (redoBtn) {
159
+ redoBtn.addEventListener('click', () => {
160
+ editor.UndoManager.redo();
161
+ });
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Setup save functionality
167
+ * @param {Object} editor - GrapeJS editor instance
168
+ * @param {Object} config - Editor configuration
169
+ * @param {string} csrfToken - CSRF token for requests
170
+ */
171
+ function setupSave(editor, config, csrfToken) {
172
+ const { showToast, showLoading } = window.ActiveCanvasEditor;
173
+
174
+ const saveBtn = document.getElementById('btn-save');
175
+ if (saveBtn) {
176
+ saveBtn.addEventListener('click', () => saveContent(false));
177
+ }
178
+
179
+ // Keyboard shortcuts
180
+ document.addEventListener('keydown', function(e) {
181
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
182
+ e.preventDefault();
183
+ saveContent(false);
184
+ }
185
+ });
186
+
187
+ // Auto-save every 60 seconds
188
+ setInterval(() => {
189
+ saveContent(true);
190
+ }, 60000);
191
+
192
+ function saveContent(isAutoSave) {
193
+ const saveBtn = document.getElementById('btn-save');
194
+ if (saveBtn) saveBtn.disabled = true;
195
+
196
+ const html = editor.getHtml();
197
+ const css = editor.getCss();
198
+ const js = window.ActiveCanvasEditor.getJs ? window.ActiveCanvasEditor.getJs() : '';
199
+ const components = JSON.stringify(editor.getComponents());
200
+
201
+ // Use entityType from config (defaults to 'page' for backwards compatibility)
202
+ const entityType = config.entityType || 'page';
203
+ const payload = {};
204
+ payload[entityType] = {
205
+ content: html,
206
+ content_css: css,
207
+ content_js: js,
208
+ content_components: components
209
+ };
210
+
211
+ fetch(config.saveUrl, {
212
+ method: 'PATCH',
213
+ headers: {
214
+ 'Content-Type': 'application/json',
215
+ 'X-CSRF-Token': csrfToken,
216
+ 'Accept': 'application/json'
217
+ },
218
+ body: JSON.stringify(payload)
219
+ })
220
+ .then(response => response.json())
221
+ .then(result => {
222
+ if (result.success) {
223
+ // Build save message with Tailwind compilation info
224
+ let message = isAutoSave ? 'Auto-saved' : 'Page saved successfully';
225
+
226
+ if (result.tailwind && result.tailwind.compiled) {
227
+ if (result.tailwind.success) {
228
+ const sizeKb = (result.tailwind.css_size / 1024).toFixed(1);
229
+ message += ` · Tailwind compiled (${sizeKb}KB in ${result.tailwind.elapsed_ms}ms)`;
230
+ } else {
231
+ showToast('Page saved, but Tailwind compilation failed: ' + result.tailwind.error, 'warning');
232
+ return;
233
+ }
234
+ }
235
+
236
+ showToast(message, 'success');
237
+ } else {
238
+ showToast(result.errors ? result.errors.join(', ') : 'Save failed', 'error');
239
+ }
240
+ })
241
+ .catch(error => {
242
+ showToast('Save failed', 'error');
243
+ console.error('Save error:', error);
244
+ })
245
+ .finally(() => {
246
+ if (saveBtn) saveBtn.disabled = false;
247
+ });
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Setup add section button
253
+ * @param {Object} editor - GrapeJS editor instance
254
+ */
255
+ function setupAddSection(editor) {
256
+ const btnAddSection = document.getElementById('btn-add-section');
257
+
258
+ if (!btnAddSection) return;
259
+
260
+ btnAddSection.addEventListener('click', function() {
261
+ // Add a new empty section at the end of the page
262
+ const wrapper = editor.getWrapper();
263
+
264
+ const section = wrapper.append({
265
+ tagName: 'section',
266
+ classes: ['ac-section'],
267
+ style: {
268
+ 'min-height': '100px',
269
+ 'padding': '2rem'
270
+ },
271
+ content: ''
272
+ })[0];
273
+
274
+ // Select the new section
275
+ editor.select(section);
276
+
277
+ // Scroll to the new section in the canvas
278
+ const frame = editor.Canvas.getFrameEl();
279
+ if (frame && frame.contentWindow) {
280
+ const el = section.getEl();
281
+ if (el) {
282
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
283
+ }
284
+ }
285
+ });
286
+ }
287
+
288
+ /**
289
+ * Setup canvas injection for global CSS/JS
290
+ * @param {Object} editor - GrapeJS editor instance
291
+ * @param {Object} config - Editor configuration
292
+ */
293
+ function setupCanvasInjection(editor, config) {
294
+ editor.on('load', () => {
295
+ setTimeout(() => {
296
+ const frame = editor.Canvas.getFrameEl();
297
+ if (!frame || !frame.contentDocument) return;
298
+
299
+ // Inject Tailwind config before the CDN script (if using Tailwind)
300
+ if (config.cssFramework === 'tailwind' && config.tailwindConfig) {
301
+ const tailwindConfigScript = frame.contentDocument.createElement('script');
302
+ tailwindConfigScript.id = 'active-canvas-tailwind-config';
303
+ tailwindConfigScript.textContent = 'tailwind.config = ' + JSON.stringify(config.tailwindConfig) + ';';
304
+ // Insert at the beginning of head so it's available before Tailwind CDN loads
305
+ frame.contentDocument.head.insertBefore(tailwindConfigScript, frame.contentDocument.head.firstChild);
306
+ }
307
+
308
+ // Inject editor-specific CSS (for empty sections, etc.)
309
+ const editorStyle = frame.contentDocument.createElement('style');
310
+ editorStyle.id = 'active-canvas-editor-css';
311
+ editorStyle.textContent = `
312
+ /* Empty section placeholder styling */
313
+ section:empty,
314
+ .ac-section:empty,
315
+ [data-gjs-type="section"]:empty {
316
+ min-height: 100px !important;
317
+ background-color: #fef2f2 !important;
318
+ border: 2px dashed #fca5a5 !important;
319
+ display: flex !important;
320
+ align-items: center !important;
321
+ justify-content: center !important;
322
+ position: relative;
323
+ }
324
+ section:empty::after,
325
+ .ac-section:empty::after,
326
+ [data-gjs-type="section"]:empty::after {
327
+ content: "Empty section - drag content here";
328
+ color: #f87171;
329
+ font-size: 14px;
330
+ font-weight: 500;
331
+ }
332
+ `;
333
+ frame.contentDocument.head.appendChild(editorStyle);
334
+
335
+ // Inject global CSS
336
+ if (config.globalCss && config.globalCss.trim()) {
337
+ const globalStyle = frame.contentDocument.createElement('style');
338
+ globalStyle.id = 'active-canvas-global-css';
339
+ globalStyle.textContent = config.globalCss;
340
+ frame.contentDocument.head.appendChild(globalStyle);
341
+ }
342
+
343
+ // Inject global JS
344
+ if (config.globalJs && config.globalJs.trim()) {
345
+ const globalScript = frame.contentDocument.createElement('script');
346
+ globalScript.id = 'active-canvas-global-js';
347
+ globalScript.textContent = config.globalJs;
348
+ frame.contentDocument.body.appendChild(globalScript);
349
+ }
350
+
351
+ // Inject page-specific JS
352
+ if (config.contentJs && config.contentJs.trim()) {
353
+ const script = frame.contentDocument.createElement('script');
354
+ script.id = 'active-canvas-custom-js';
355
+ script.textContent = config.contentJs;
356
+ frame.contentDocument.body.appendChild(script);
357
+ }
358
+ }, 500);
359
+ });
360
+ }
361
+
362
+ /**
363
+ * Setup RTE toolbar visibility management
364
+ * Hides the toolbar when not actively editing text
365
+ * @param {Object} editor - GrapeJS editor instance
366
+ */
367
+ function setupRteToolbar(editor) {
368
+ let rteActive = false;
369
+
370
+ // Function to hide RTE toolbar
371
+ const hideRteToolbar = () => {
372
+ rteActive = false;
373
+ const rteToolbar = document.querySelector('.gjs-rte-toolbar');
374
+ if (rteToolbar) {
375
+ rteToolbar.classList.add('ac-rte-hidden');
376
+ }
377
+ };
378
+
379
+ // Function to show RTE toolbar
380
+ const showRteToolbar = () => {
381
+ const rteToolbar = document.querySelector('.gjs-rte-toolbar');
382
+ if (rteToolbar) {
383
+ rteToolbar.classList.remove('ac-rte-hidden');
384
+ }
385
+ };
386
+
387
+ // Check if component is a text element
388
+ const isTextComponent = (component) => {
389
+ if (!component) return false;
390
+ const type = component.get('type');
391
+ const tagName = (component.get('tagName') || '').toLowerCase();
392
+
393
+ // Text component types
394
+ const textTypes = ['text', 'textnode', 'label'];
395
+ if (textTypes.includes(type)) return true;
396
+
397
+ // Text-like HTML tags
398
+ const textTags = ['p', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'label', 'li', 'td', 'th', 'blockquote', 'cite', 'em', 'strong', 'b', 'i', 'u'];
399
+ if (textTags.includes(tagName)) return true;
400
+
401
+ return false;
402
+ };
403
+
404
+ // Track RTE state
405
+ editor.on('rte:enable', () => {
406
+ rteActive = true;
407
+ showRteToolbar();
408
+ });
409
+
410
+ editor.on('rte:disable', () => {
411
+ hideRteToolbar();
412
+ });
413
+
414
+ // Hide when selecting a non-text component
415
+ editor.on('component:selected', (component) => {
416
+ if (!isTextComponent(component)) {
417
+ hideRteToolbar();
418
+ // Also disable RTE if it was active
419
+ if (rteActive) {
420
+ editor.RichTextEditor.disable();
421
+ }
422
+ }
423
+ });
424
+
425
+ // Hide when deselecting
426
+ editor.on('component:deselected', () => {
427
+ hideRteToolbar();
428
+ });
429
+
430
+ // Initial hide after editor loads
431
+ editor.on('load', () => {
432
+ setTimeout(hideRteToolbar, 100);
433
+
434
+ // Set up a MutationObserver to catch the toolbar being created
435
+ const observer = new MutationObserver((mutations) => {
436
+ mutations.forEach((mutation) => {
437
+ mutation.addedNodes.forEach((node) => {
438
+ if (node.classList && node.classList.contains('gjs-rte-toolbar')) {
439
+ if (!rteActive) {
440
+ node.classList.add('ac-rte-hidden');
441
+ }
442
+ }
443
+ });
444
+ });
445
+ });
446
+
447
+ observer.observe(document.body, { childList: true, subtree: true });
448
+ });
449
+ }
450
+
451
+ // Expose functions
452
+ window.ActiveCanvasEditor.setupPanelControls = setupPanelControls;
453
+ window.ActiveCanvasEditor.setupDeviceSwitching = setupDeviceSwitching;
454
+ window.ActiveCanvasEditor.setupUndoRedo = setupUndoRedo;
455
+ window.ActiveCanvasEditor.setupSave = setupSave;
456
+ window.ActiveCanvasEditor.setupAddSection = setupAddSection;
457
+ window.ActiveCanvasEditor.setupCanvasInjection = setupCanvasInjection;
458
+ window.ActiveCanvasEditor.setupRteToolbar = setupRteToolbar;
459
+
460
+ })();
@@ -0,0 +1,56 @@
1
+ /**
2
+ * ActiveCanvas Editor - Utility Functions
3
+ */
4
+
5
+ (function() {
6
+ 'use strict';
7
+
8
+ window.ActiveCanvasEditor = window.ActiveCanvasEditor || {};
9
+
10
+ /**
11
+ * Show a toast notification
12
+ * @param {string} message - The message to display
13
+ * @param {string} type - 'success' or 'error'
14
+ */
15
+ function showToast(message, type) {
16
+ const toast = document.getElementById('editor-toast');
17
+ if (!toast) return;
18
+
19
+ toast.textContent = message;
20
+ toast.className = 'editor-toast show ' + type;
21
+
22
+ setTimeout(() => {
23
+ toast.classList.remove('show');
24
+ }, 3000);
25
+ }
26
+
27
+ /**
28
+ * Show or hide the loading overlay
29
+ * @param {boolean} show - Whether to show the loading overlay
30
+ */
31
+ function showLoading(show) {
32
+ const loading = document.getElementById('editor-loading');
33
+ if (!loading) return;
34
+
35
+ if (show) {
36
+ loading.classList.remove('hidden');
37
+ } else {
38
+ loading.classList.add('hidden');
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Get CSRF token from meta tag
44
+ * @returns {string} The CSRF token
45
+ */
46
+ function getCsrfToken() {
47
+ const meta = document.querySelector('meta[name="csrf-token"]');
48
+ return meta ? meta.getAttribute('content') : '';
49
+ }
50
+
51
+ // Expose utilities
52
+ window.ActiveCanvasEditor.showToast = showToast;
53
+ window.ActiveCanvasEditor.showLoading = showLoading;
54
+ window.ActiveCanvasEditor.getCsrfToken = getCsrfToken;
55
+
56
+ })();