active_canvas 0.0.5 → 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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +23 -1
  3. data/app/assets/javascripts/active_canvas/admin/data_panel.js +357 -0
  4. data/app/assets/javascripts/active_canvas/admin/field_builder.js +84 -0
  5. data/app/assets/javascripts/active_canvas/admin/media_select_preview.js +11 -0
  6. data/app/assets/javascripts/active_canvas/editor/ac_bindings.js +81 -0
  7. data/app/assets/javascripts/active_canvas/editor/ac_chips.js +192 -0
  8. data/app/assets/javascripts/active_canvas/editor/code_panel.js +10 -7
  9. data/app/assets/javascripts/active_canvas/editor/directive_traits.js +95 -0
  10. data/app/assets/javascripts/active_canvas/editor/live_data.js +178 -0
  11. data/app/assets/javascripts/active_canvas/editor/panels.js +23 -7
  12. data/app/assets/javascripts/active_canvas/editor/preview_modal.js +112 -0
  13. data/app/assets/javascripts/active_canvas/editor/template_validator.js +95 -0
  14. data/app/assets/javascripts/active_canvas/editor.js +21 -3
  15. data/app/assets/stylesheets/active_canvas/editor.css +626 -0
  16. data/app/controllers/active_canvas/admin/application_controller.rb +8 -0
  17. data/app/controllers/active_canvas/admin/collection_items_controller.rb +93 -0
  18. data/app/controllers/active_canvas/admin/collections_controller.rb +51 -0
  19. data/app/controllers/active_canvas/admin/form_submissions_controller.rb +51 -0
  20. data/app/controllers/active_canvas/admin/pages_controller.rb +127 -5
  21. data/app/controllers/active_canvas/form_submissions_controller.rb +90 -0
  22. data/app/controllers/active_canvas/pages_controller.rb +8 -0
  23. data/app/controllers/active_canvas/redirects_controller.rb +10 -0
  24. data/app/helpers/active_canvas/admin/collection_items_helper.rb +33 -0
  25. data/app/models/active_canvas/auto_drop.rb +69 -0
  26. data/app/models/active_canvas/collection.rb +98 -0
  27. data/app/models/active_canvas/collection_item.rb +66 -0
  28. data/app/models/active_canvas/collection_item_version.rb +18 -0
  29. data/app/models/active_canvas/current.rb +2 -0
  30. data/app/models/active_canvas/form_submission.rb +9 -0
  31. data/app/models/active_canvas/page.rb +76 -15
  32. data/app/models/active_canvas/page_redirect.rb +7 -0
  33. data/app/services/active_canvas/collection_schema.rb +110 -0
  34. data/app/services/active_canvas/collection_source.rb +81 -0
  35. data/app/services/active_canvas/content_sanitizer.rb +33 -21
  36. data/app/services/active_canvas/form_schema.rb +48 -0
  37. data/app/services/active_canvas/form_stamper.rb +86 -0
  38. data/app/services/active_canvas/template_renderer/binding_resolver.rb +138 -0
  39. data/app/services/active_canvas/template_renderer/directive_expander.rb +54 -0
  40. data/app/services/active_canvas/template_renderer.rb +160 -0
  41. data/app/views/active_canvas/admin/collection_items/_field_input.html.erb +40 -0
  42. data/app/views/active_canvas/admin/collection_items/_form.html.erb +28 -0
  43. data/app/views/active_canvas/admin/collection_items/edit.html.erb +24 -0
  44. data/app/views/active_canvas/admin/collection_items/history.html.erb +34 -0
  45. data/app/views/active_canvas/admin/collection_items/index.html.erb +63 -0
  46. data/app/views/active_canvas/admin/collection_items/new.html.erb +9 -0
  47. data/app/views/active_canvas/admin/collections/_field_builder.html.erb +35 -0
  48. data/app/views/active_canvas/admin/collections/_form.html.erb +31 -0
  49. data/app/views/active_canvas/admin/collections/edit.html.erb +10 -0
  50. data/app/views/active_canvas/admin/collections/index.html.erb +45 -0
  51. data/app/views/active_canvas/admin/collections/new.html.erb +9 -0
  52. data/app/views/active_canvas/admin/form_submissions/index.html.erb +80 -0
  53. data/app/views/active_canvas/admin/form_submissions/show.html.erb +99 -0
  54. data/app/views/active_canvas/admin/pages/_data_panel.html.erb +81 -0
  55. data/app/views/active_canvas/admin/pages/_form.html.erb +10 -0
  56. data/app/views/active_canvas/admin/pages/editor.html.erb +62 -1
  57. data/app/views/active_canvas/pages/show.html.erb +4 -1
  58. data/app/views/layouts/active_canvas/admin/application.html.erb +15 -0
  59. data/config/routes.rb +20 -0
  60. data/db/migrate/20260517100001_add_template_enabled_and_bindings_to_active_canvas_pages.rb +6 -0
  61. data/db/migrate/20260517100002_add_bindings_to_active_canvas_page_versions.rb +6 -0
  62. data/db/migrate/20260702000001_create_active_canvas_page_redirects.rb +11 -0
  63. data/db/migrate/20260702000002_create_active_canvas_form_submissions.rb +14 -0
  64. data/db/migrate/20260702000003_create_active_canvas_collections.rb +12 -0
  65. data/db/migrate/20260702000004_create_active_canvas_collection_items.rb +16 -0
  66. data/db/migrate/20260702000005_create_active_canvas_collection_item_versions.rb +17 -0
  67. data/lib/active_canvas/configuration.rb +32 -1
  68. data/lib/active_canvas/data_sources/errors.rb +51 -0
  69. data/lib/active_canvas/data_sources/param_spec.rb +64 -0
  70. data/lib/active_canvas/data_sources/registration.rb +60 -0
  71. data/lib/active_canvas/data_sources/source.rb +47 -0
  72. data/lib/active_canvas/data_sources.rb +54 -0
  73. data/lib/active_canvas/engine.rb +15 -0
  74. data/lib/active_canvas/version.rb +1 -1
  75. data/lib/active_canvas.rb +6 -0
  76. metadata +85 -3
@@ -0,0 +1,112 @@
1
+ /**
2
+ * ActiveCanvas Editor - Preview modal
3
+ *
4
+ * Renders the editor's unsaved state through the public view (preview_iframe)
5
+ * into a sandboxed iframe. Requests are sequenced so a stale response never
6
+ * replaces a newer one, and closing aborts the in-flight request.
7
+ */
8
+ (function() {
9
+ 'use strict';
10
+
11
+ function init() {
12
+ const btn = document.getElementById('btn-preview');
13
+ const modal = document.getElementById('preview-modal');
14
+ if (!btn || !modal) return;
15
+
16
+ const iframe = document.getElementById('preview-modal-iframe');
17
+ const loading = document.getElementById('preview-modal-loading');
18
+ const errorBox = document.getElementById('preview-modal-error');
19
+ const errorMsg = document.getElementById('preview-modal-error-message');
20
+ const closeBtn = document.getElementById('btn-close-preview');
21
+ const previewUrl = btn.dataset.previewUrl;
22
+
23
+ let seq = 0;
24
+ let controller = null;
25
+ let loadingTimer = null;
26
+
27
+ btn.addEventListener('click', open);
28
+ closeBtn.addEventListener('click', close);
29
+ modal.addEventListener('click', e => { if (e.target === modal) close(); });
30
+ document.addEventListener('keydown', e => {
31
+ if (isOpen() && e.key === 'Escape') { close(); return; }
32
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'p') {
33
+ if (document.activeElement && document.activeElement.closest('.monaco-editor')) return;
34
+ e.preventDefault();
35
+ if (!isOpen()) open();
36
+ }
37
+ });
38
+
39
+ function isOpen() { return !modal.hasAttribute('hidden'); }
40
+
41
+ function open() {
42
+ const editor = window.ActiveCanvasEditor && window.ActiveCanvasEditor.instance;
43
+ modal.removeAttribute('hidden');
44
+ document.body.style.overflow = 'hidden';
45
+
46
+ if (!editor) {
47
+ showError('Editor not ready yet — try again in a second.');
48
+ return;
49
+ }
50
+
51
+ showLoading();
52
+ const id = ++seq;
53
+ if (controller) controller.abort();
54
+ controller = new AbortController();
55
+
56
+ const html = editor.getHtml();
57
+ const content = window.ActiveCanvasChips ? window.ActiveCanvasChips.undecorate(html) : html;
58
+ const csrf = document.querySelector('meta[name="csrf-token"]');
59
+
60
+ fetch(previewUrl, {
61
+ method: 'POST',
62
+ signal: controller.signal,
63
+ headers: {
64
+ 'Content-Type': 'application/x-www-form-urlencoded',
65
+ 'X-CSRF-Token': csrf ? csrf.content : '',
66
+ 'Accept': 'application/json'
67
+ },
68
+ body: new URLSearchParams({
69
+ content: content,
70
+ content_css: editor.getCss(),
71
+ content_js: (window.ActiveCanvasEditor && window.ActiveCanvasEditor.getJs) ? window.ActiveCanvasEditor.getJs() : '',
72
+ bindings: window.ActiveCanvasBindings ? window.ActiveCanvasBindings.readJson() : '{}'
73
+ }).toString()
74
+ })
75
+ .then(r => r.json().then(body => ({ ok: r.ok, body })))
76
+ .then(({ ok, body }) => {
77
+ if (id !== seq || !isOpen()) return;
78
+ if (ok && body.html) {
79
+ iframe.onload = hideLoading;
80
+ iframe.srcdoc = body.html;
81
+ clearTimeout(loadingTimer);
82
+ loadingTimer = setTimeout(hideLoading, 1500); // some browsers skip onload for srcdoc
83
+ } else {
84
+ showError((body && body.error && body.error.message) || 'Render failed');
85
+ }
86
+ })
87
+ .catch(err => {
88
+ if (err.name === 'AbortError' || id !== seq) return;
89
+ showError(err.message || 'Network error');
90
+ });
91
+ }
92
+
93
+ function close() {
94
+ seq++; // any response still in flight is now stale
95
+ if (controller) { controller.abort(); controller = null; }
96
+ clearTimeout(loadingTimer);
97
+ modal.setAttribute('hidden', '');
98
+ document.body.style.overflow = '';
99
+ iframe.onload = null;
100
+ iframe.srcdoc = '';
101
+ hideError();
102
+ hideLoading();
103
+ }
104
+
105
+ function showLoading() { loading.removeAttribute('hidden'); hideError(); }
106
+ function hideLoading() { loading.setAttribute('hidden', ''); }
107
+ function showError(message) { hideLoading(); errorMsg.textContent = message; errorBox.removeAttribute('hidden'); }
108
+ function hideError() { errorBox.setAttribute('hidden', ''); }
109
+ }
110
+
111
+ document.addEventListener('DOMContentLoaded', init);
112
+ })();
@@ -0,0 +1,95 @@
1
+ /**
2
+ * ActiveCanvas Editor - Template validator
3
+ *
4
+ * Posts the current source and bindings to validate_template after the
5
+ * canvas or the bindings change (debounced) and shows the first error in a
6
+ * banner above the canvas, with line and column. Never touches the canvas.
7
+ */
8
+ (function() {
9
+ 'use strict';
10
+
11
+ window.ActiveCanvasEditor = window.ActiveCanvasEditor || {};
12
+
13
+ function setupTemplateValidator(editor, config) {
14
+ const container = document.getElementById('data-panel-container');
15
+ if (!container || !config.templateEnabled) return;
16
+
17
+ const url = container.dataset.validateUrl;
18
+ const csrf = window.ActiveCanvasEditor.getCsrfToken();
19
+ let seq = 0;
20
+ let timer = null;
21
+ let lastPayload = null;
22
+
23
+ // Only Liquid tags and directives can change the validation result.
24
+ function liquidFingerprint(content) {
25
+ return (content.match(/\{\{[\s\S]*?\}\}|\{%[\s\S]*?%\}|data-ac-(?:for|if)="[^"]*"/g) || []).join('\n');
26
+ }
27
+
28
+ function currentPayload() {
29
+ const html = editor.getHtml();
30
+ const content = window.ActiveCanvasChips ? window.ActiveCanvasChips.undecorate(html) : html;
31
+ const bindingsJson = window.ActiveCanvasBindings.readJson();
32
+ return { payload: JSON.stringify({ content, bindings: bindingsJson }), fingerprint: liquidFingerprint(content) + '\n' + bindingsJson };
33
+ }
34
+
35
+ function validate(force) {
36
+ const { payload, fingerprint } = currentPayload();
37
+ if (!force && fingerprint === lastPayload) return;
38
+ lastPayload = fingerprint;
39
+ const id = ++seq;
40
+
41
+ fetch(url, {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, 'Accept': 'application/json' },
44
+ body: payload
45
+ })
46
+ .then(r => r.json().then(body => ({ ok: r.ok, body })))
47
+ .then(({ ok, body }) => {
48
+ if (id !== seq) return; // a newer check is in flight
49
+ if (ok && body.ok) clearError();
50
+ else showError(body.error || { message: 'Template check failed' });
51
+ })
52
+ .catch(() => {
53
+ if (id === seq) showError({ message: 'Could not reach the server to check the template' });
54
+ });
55
+ }
56
+
57
+ function schedule() {
58
+ clearTimeout(timer);
59
+ timer = setTimeout(() => validate(false), 2000);
60
+ }
61
+
62
+ editor.on('load', () => validate(true));
63
+ editor.on('component:update component:add component:remove', schedule);
64
+ document.addEventListener('ac:bindings-changed', () => validate(true));
65
+
66
+ function showError(err) {
67
+ let banner = document.getElementById('ac-error-banner');
68
+ if (!banner) {
69
+ banner = document.createElement('div');
70
+ banner.id = 'ac-error-banner';
71
+ banner.className = 'ac-error-banner';
72
+ (document.querySelector('.editor-canvas') || document.body).prepend(banner);
73
+ }
74
+ const where = err.line ? ` (line ${err.line}${err.column ? `, column ${err.column}` : ''})` : '';
75
+ const message = String(err.message || '').replace(/^Liquid (?:syntax )?error \(line \d+\): /, '');
76
+ banner.innerHTML = '';
77
+ const text = document.createElement('span');
78
+ text.textContent = `Template error: ${message}${where}`;
79
+ const close = document.createElement('button');
80
+ close.type = 'button';
81
+ close.className = 'ac-error-banner-close';
82
+ close.title = 'Dismiss';
83
+ close.textContent = '×';
84
+ close.addEventListener('click', clearError);
85
+ banner.append(text, close);
86
+ }
87
+
88
+ function clearError() {
89
+ const banner = document.getElementById('ac-error-banner');
90
+ if (banner) banner.remove();
91
+ }
92
+ }
93
+
94
+ window.ActiveCanvasEditor.setupTemplateValidator = setupTemplateValidator;
95
+ })();
@@ -38,9 +38,12 @@
38
38
  function initEditor(config) {
39
39
  const csrfToken = window.ActiveCanvasEditor.getCsrfToken();
40
40
 
41
- // Parse components JSON if available, otherwise use HTML content
41
+ // Dynamic pages always load their Liquid SOURCE, decorated with chips;
42
+ // the components JSON is not used for them (it is not persisted either).
42
43
  let componentsToLoad = config.content || '';
43
- if (config.contentComponents) {
44
+ if (config.templateEnabled && window.ActiveCanvasChips) {
45
+ componentsToLoad = window.ActiveCanvasChips.decorate(componentsToLoad);
46
+ } else if (config.contentComponents) {
44
47
  try {
45
48
  componentsToLoad = JSON.parse(config.contentComponents);
46
49
  } catch (e) {
@@ -66,6 +69,11 @@
66
69
  // Storage - we'll handle saving manually
67
70
  storageManager: false,
68
71
 
72
+ // Text blocks that contain a Liquid chip must still be editable text.
73
+ parser: {
74
+ textTypes: ['text', 'textnode', 'comment', 'ac-chip']
75
+ },
76
+
69
77
  // Device Manager
70
78
  deviceManager: {
71
79
  devices: [
@@ -147,7 +155,8 @@
147
155
  'grapesjs-touch',
148
156
  'grapesjs-parser-postcss',
149
157
  'grapesjs-tooltip',
150
- 'grapesjs-typed'
158
+ 'grapesjs-typed',
159
+ window.ActiveCanvasChips ? window.ActiveCanvasChips.plugin : function() {}
151
160
  ],
152
161
 
153
162
  pluginsOpts: {
@@ -285,6 +294,15 @@
285
294
  // Setup RTE toolbar visibility
286
295
  AC.setupRteToolbar(editor);
287
296
 
297
+ // Loop / condition traits for dynamic pages
298
+ if (AC.setupDirectiveTraits) AC.setupDirectiveTraits(editor, config);
299
+
300
+ // Server-side template check with an error banner
301
+ if (AC.setupTemplateValidator) AC.setupTemplateValidator(editor, config);
302
+
303
+ // Live values inside the canvas for dynamic pages
304
+ if (AC.setupLiveData) AC.setupLiveData(editor, config);
305
+
288
306
  // Setup assets panel
289
307
  AC.setupAssetsPanel(editor, config, csrfToken);
290
308