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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +318 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/active_canvas/editor/ai_panel.js +1607 -0
- data/app/assets/javascripts/active_canvas/editor/asset_manager.js +498 -0
- data/app/assets/javascripts/active_canvas/editor/blocks.js +1083 -0
- data/app/assets/javascripts/active_canvas/editor/code_panel.js +572 -0
- data/app/assets/javascripts/active_canvas/editor/component_toolbar.js +394 -0
- data/app/assets/javascripts/active_canvas/editor/panels.js +460 -0
- data/app/assets/javascripts/active_canvas/editor/utils.js +56 -0
- data/app/assets/javascripts/active_canvas/editor.js +295 -0
- data/app/assets/stylesheets/active_canvas/application.css +15 -0
- data/app/assets/stylesheets/active_canvas/editor.css +2929 -0
- data/app/controllers/active_canvas/admin/ai_controller.rb +181 -0
- data/app/controllers/active_canvas/admin/application_controller.rb +56 -0
- data/app/controllers/active_canvas/admin/media_controller.rb +61 -0
- data/app/controllers/active_canvas/admin/page_types_controller.rb +57 -0
- data/app/controllers/active_canvas/admin/page_versions_controller.rb +23 -0
- data/app/controllers/active_canvas/admin/pages_controller.rb +133 -0
- data/app/controllers/active_canvas/admin/partials_controller.rb +88 -0
- data/app/controllers/active_canvas/admin/settings_controller.rb +256 -0
- data/app/controllers/active_canvas/application_controller.rb +20 -0
- data/app/controllers/active_canvas/pages_controller.rb +18 -0
- data/app/controllers/concerns/active_canvas/current_user.rb +12 -0
- data/app/controllers/concerns/active_canvas/rate_limitable.rb +75 -0
- data/app/controllers/concerns/active_canvas/tailwind_compilation.rb +39 -0
- data/app/helpers/active_canvas/application_helper.rb +4 -0
- data/app/jobs/active_canvas/application_job.rb +4 -0
- data/app/jobs/active_canvas/compile_tailwind_job.rb +64 -0
- data/app/mailers/active_canvas/application_mailer.rb +6 -0
- data/app/models/active_canvas/ai_model.rb +136 -0
- data/app/models/active_canvas/application_record.rb +5 -0
- data/app/models/active_canvas/media.rb +141 -0
- data/app/models/active_canvas/page.rb +85 -0
- data/app/models/active_canvas/page_type.rb +22 -0
- data/app/models/active_canvas/page_version.rb +80 -0
- data/app/models/active_canvas/partial.rb +73 -0
- data/app/models/active_canvas/setting.rb +292 -0
- data/app/services/active_canvas/ai_configuration.rb +40 -0
- data/app/services/active_canvas/ai_models.rb +128 -0
- data/app/services/active_canvas/ai_service.rb +289 -0
- data/app/services/active_canvas/content_sanitizer.rb +112 -0
- data/app/services/active_canvas/tailwind_compiler.rb +156 -0
- data/app/views/active_canvas/admin/media/index.html.erb +401 -0
- data/app/views/active_canvas/admin/media/show.html.erb +297 -0
- data/app/views/active_canvas/admin/page_types/_form.html.erb +25 -0
- data/app/views/active_canvas/admin/page_types/edit.html.erb +13 -0
- data/app/views/active_canvas/admin/page_types/index.html.erb +29 -0
- data/app/views/active_canvas/admin/page_types/new.html.erb +9 -0
- data/app/views/active_canvas/admin/page_types/show.html.erb +18 -0
- data/app/views/active_canvas/admin/page_versions/show.html.erb +469 -0
- data/app/views/active_canvas/admin/pages/_form.html.erb +62 -0
- data/app/views/active_canvas/admin/pages/content.html.erb +139 -0
- data/app/views/active_canvas/admin/pages/edit.html.erb +335 -0
- data/app/views/active_canvas/admin/pages/editor.html.erb +710 -0
- data/app/views/active_canvas/admin/pages/index.html.erb +149 -0
- data/app/views/active_canvas/admin/pages/new.html.erb +19 -0
- data/app/views/active_canvas/admin/pages/show.html.erb +258 -0
- data/app/views/active_canvas/admin/pages/versions.html.erb +333 -0
- data/app/views/active_canvas/admin/partials/edit.html.erb +182 -0
- data/app/views/active_canvas/admin/partials/editor.html.erb +703 -0
- data/app/views/active_canvas/admin/partials/index.html.erb +131 -0
- data/app/views/active_canvas/admin/settings/show.html.erb +1864 -0
- data/app/views/active_canvas/pages/no_homepage.html.erb +45 -0
- data/app/views/active_canvas/pages/show.html.erb +113 -0
- data/app/views/layouts/active_canvas/admin/application.html.erb +960 -0
- data/app/views/layouts/active_canvas/admin/editor.html.erb +826 -0
- data/app/views/layouts/active_canvas/application.html.erb +55 -0
- data/config/routes.rb +48 -0
- data/db/migrate/20260202000001_create_active_canvas_tables.rb +113 -0
- data/db/migrate/20260202000002_create_active_canvas_ai_models.rb +26 -0
- data/lib/active_canvas/configuration.rb +232 -0
- data/lib/active_canvas/engine.rb +44 -0
- data/lib/active_canvas/version.rb +3 -0
- data/lib/active_canvas.rb +26 -0
- data/lib/generators/active_canvas/install/install_generator.rb +263 -0
- data/lib/generators/active_canvas/install/templates/initializer.rb.tt +163 -0
- data/lib/tasks/active_canvas_tasks.rake +69 -0
- metadata +150 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActiveCanvas Editor - Custom Asset Manager Modal
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
(function() {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
window.ActiveCanvasEditor = window.ActiveCanvasEditor || {};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Setup the custom asset manager modal
|
|
12
|
+
* @param {Object} editor - GrapeJS editor instance
|
|
13
|
+
* @param {Object} config - Editor configuration
|
|
14
|
+
* @param {string} csrfToken - CSRF token for requests
|
|
15
|
+
*/
|
|
16
|
+
function setupCustomAssetManager(editor, config, csrfToken) {
|
|
17
|
+
const { showToast } = window.ActiveCanvasEditor;
|
|
18
|
+
let currentPage = 1;
|
|
19
|
+
let totalPages = 1;
|
|
20
|
+
let currentTarget = null;
|
|
21
|
+
|
|
22
|
+
// Override the default asset manager open behavior
|
|
23
|
+
editor.on('run:open-assets', () => {
|
|
24
|
+
const am = editor.AssetManager;
|
|
25
|
+
currentTarget = am.getConfig().target;
|
|
26
|
+
openCustomAssetModal();
|
|
27
|
+
return false;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Also listen for asset manager open command
|
|
31
|
+
const originalOpen = editor.AssetManager.open;
|
|
32
|
+
editor.AssetManager.open = function(options) {
|
|
33
|
+
currentTarget = options?.target;
|
|
34
|
+
openCustomAssetModal();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function openCustomAssetModal() {
|
|
38
|
+
// Remove existing modal if any
|
|
39
|
+
const existing = document.querySelector('.ac-asset-modal');
|
|
40
|
+
if (existing) existing.remove();
|
|
41
|
+
|
|
42
|
+
// Create modal
|
|
43
|
+
const modal = document.createElement('div');
|
|
44
|
+
modal.className = 'ac-asset-modal';
|
|
45
|
+
modal.innerHTML = `
|
|
46
|
+
<div class="ac-asset-modal-overlay"></div>
|
|
47
|
+
<div class="ac-asset-modal-dialog">
|
|
48
|
+
<div class="ac-asset-modal-header">
|
|
49
|
+
<h3>Select Image</h3>
|
|
50
|
+
<button class="ac-asset-modal-close" title="Close">
|
|
51
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
52
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
53
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
54
|
+
</svg>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="ac-asset-modal-tabs">
|
|
58
|
+
<button class="ac-asset-tab active" data-tab="library">Media Library</button>
|
|
59
|
+
<button class="ac-asset-tab" data-tab="upload">Upload New</button>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="ac-asset-modal-body">
|
|
62
|
+
<div class="ac-asset-tab-content active" data-tab="library">
|
|
63
|
+
<div class="ac-asset-grid" id="ac-asset-grid">
|
|
64
|
+
<div class="ac-asset-loading">Loading media...</div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="ac-asset-pagination" id="ac-asset-pagination"></div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="ac-asset-tab-content" data-tab="upload">
|
|
69
|
+
<div class="ac-asset-upload-zone" id="ac-upload-zone">
|
|
70
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
71
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
72
|
+
<polyline points="17 8 12 3 7 8"/>
|
|
73
|
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
74
|
+
</svg>
|
|
75
|
+
<p>Drag & drop images here or click to browse</p>
|
|
76
|
+
<p class="ac-upload-hint">Supports: JPEG, PNG, GIF, WebP, SVG</p>
|
|
77
|
+
<input type="file" id="ac-upload-input" accept="image/*" multiple style="display:none">
|
|
78
|
+
</div>
|
|
79
|
+
<div class="ac-upload-progress" id="ac-upload-progress" style="display:none">
|
|
80
|
+
<div class="ac-upload-progress-bar"></div>
|
|
81
|
+
<span class="ac-upload-progress-text">Uploading...</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
document.body.appendChild(modal);
|
|
89
|
+
setupModalHandlers(modal);
|
|
90
|
+
loadMediaPage(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function setupModalHandlers(modal) {
|
|
94
|
+
// Close button
|
|
95
|
+
modal.querySelector('.ac-asset-modal-close').addEventListener('click', () => {
|
|
96
|
+
modal.remove();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Overlay click to close
|
|
100
|
+
modal.querySelector('.ac-asset-modal-overlay').addEventListener('click', () => {
|
|
101
|
+
modal.remove();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Tab switching
|
|
105
|
+
modal.querySelectorAll('.ac-asset-tab').forEach(tab => {
|
|
106
|
+
tab.addEventListener('click', () => {
|
|
107
|
+
modal.querySelectorAll('.ac-asset-tab').forEach(t => t.classList.remove('active'));
|
|
108
|
+
modal.querySelectorAll('.ac-asset-tab-content').forEach(c => c.classList.remove('active'));
|
|
109
|
+
tab.classList.add('active');
|
|
110
|
+
modal.querySelector(`.ac-asset-tab-content[data-tab="${tab.dataset.tab}"]`).classList.add('active');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Upload zone
|
|
115
|
+
const uploadZone = modal.querySelector('#ac-upload-zone');
|
|
116
|
+
const uploadInput = modal.querySelector('#ac-upload-input');
|
|
117
|
+
|
|
118
|
+
uploadZone.addEventListener('click', () => uploadInput.click());
|
|
119
|
+
|
|
120
|
+
uploadZone.addEventListener('dragover', (e) => {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
uploadZone.classList.add('dragover');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
uploadZone.addEventListener('dragleave', () => {
|
|
126
|
+
uploadZone.classList.remove('dragover');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
uploadZone.addEventListener('drop', (e) => {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
uploadZone.classList.remove('dragover');
|
|
132
|
+
handleFileUpload(e.dataTransfer.files, modal);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
uploadInput.addEventListener('change', (e) => {
|
|
136
|
+
handleFileUpload(e.target.files, modal);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function loadMediaPage(page) {
|
|
141
|
+
currentPage = page;
|
|
142
|
+
const grid = document.getElementById('ac-asset-grid');
|
|
143
|
+
const pagination = document.getElementById('ac-asset-pagination');
|
|
144
|
+
|
|
145
|
+
if (!grid) return;
|
|
146
|
+
|
|
147
|
+
grid.innerHTML = '<div class="ac-asset-loading">Loading media...</div>';
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetch(`${config.mediaUrl}?page=${page}&per_page=20`, {
|
|
151
|
+
headers: { 'Accept': 'application/json' }
|
|
152
|
+
});
|
|
153
|
+
const result = await response.json();
|
|
154
|
+
|
|
155
|
+
if (result.data && result.data.length > 0) {
|
|
156
|
+
totalPages = result.meta?.total_pages || 1;
|
|
157
|
+
renderMediaGrid(result.data, grid);
|
|
158
|
+
renderPagination(pagination, result.meta);
|
|
159
|
+
} else {
|
|
160
|
+
grid.innerHTML = '<div class="ac-asset-empty">No media found. Upload some images to get started.</div>';
|
|
161
|
+
pagination.innerHTML = '';
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('Failed to load media:', error);
|
|
165
|
+
grid.innerHTML = '<div class="ac-asset-empty">Failed to load media.</div>';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderMediaGrid(media, container) {
|
|
170
|
+
container.innerHTML = media.map(item => `
|
|
171
|
+
<div class="ac-asset-item" data-src="${item.src}" data-name="${item.name || ''}">
|
|
172
|
+
<div class="ac-asset-thumb">
|
|
173
|
+
<img src="${item.src}" alt="${item.name || 'Image'}" loading="lazy">
|
|
174
|
+
</div>
|
|
175
|
+
<div class="ac-asset-name">${item.name || 'Untitled'}</div>
|
|
176
|
+
</div>
|
|
177
|
+
`).join('');
|
|
178
|
+
|
|
179
|
+
// Add click handlers
|
|
180
|
+
container.querySelectorAll('.ac-asset-item').forEach(item => {
|
|
181
|
+
item.addEventListener('click', () => {
|
|
182
|
+
selectAsset(item.dataset.src, item.dataset.name);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderPagination(container, meta) {
|
|
188
|
+
if (!meta || meta.total_pages <= 1) {
|
|
189
|
+
container.innerHTML = '';
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let html = '<div class="ac-pagination-info">';
|
|
194
|
+
html += `Page ${meta.current_page} of ${meta.total_pages} (${meta.total_count} items)`;
|
|
195
|
+
html += '</div><div class="ac-pagination-buttons">';
|
|
196
|
+
|
|
197
|
+
if (meta.current_page > 1) {
|
|
198
|
+
html += `<button class="ac-pagination-btn" data-page="${meta.current_page - 1}">Previous</button>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Page numbers
|
|
202
|
+
const startPage = Math.max(1, meta.current_page - 2);
|
|
203
|
+
const endPage = Math.min(meta.total_pages, meta.current_page + 2);
|
|
204
|
+
|
|
205
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
206
|
+
html += `<button class="ac-pagination-btn ${i === meta.current_page ? 'active' : ''}" data-page="${i}">${i}</button>`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (meta.current_page < meta.total_pages) {
|
|
210
|
+
html += `<button class="ac-pagination-btn" data-page="${meta.current_page + 1}">Next</button>`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
html += '</div>';
|
|
214
|
+
container.innerHTML = html;
|
|
215
|
+
|
|
216
|
+
// Add click handlers
|
|
217
|
+
container.querySelectorAll('.ac-pagination-btn').forEach(btn => {
|
|
218
|
+
btn.addEventListener('click', () => {
|
|
219
|
+
loadMediaPage(parseInt(btn.dataset.page));
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function selectAsset(src, name) {
|
|
225
|
+
// Add to GrapeJS asset manager
|
|
226
|
+
editor.AssetManager.add({ src, name, type: 'image' });
|
|
227
|
+
|
|
228
|
+
// If there's a target component, set the image
|
|
229
|
+
if (currentTarget) {
|
|
230
|
+
currentTarget.set('src', src);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Close modal
|
|
234
|
+
const modal = document.querySelector('.ac-asset-modal');
|
|
235
|
+
if (modal) modal.remove();
|
|
236
|
+
|
|
237
|
+
showToast('Image selected', 'success');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function handleFileUpload(files, modal) {
|
|
241
|
+
const progressContainer = modal.querySelector('#ac-upload-progress');
|
|
242
|
+
const progressBar = modal.querySelector('.ac-upload-progress-bar');
|
|
243
|
+
const progressText = modal.querySelector('.ac-upload-progress-text');
|
|
244
|
+
|
|
245
|
+
progressContainer.style.display = 'block';
|
|
246
|
+
|
|
247
|
+
for (let i = 0; i < files.length; i++) {
|
|
248
|
+
const file = files[i];
|
|
249
|
+
progressText.textContent = `Uploading ${file.name}... (${i + 1}/${files.length})`;
|
|
250
|
+
progressBar.style.width = `${((i + 1) / files.length) * 100}%`;
|
|
251
|
+
|
|
252
|
+
const formData = new FormData();
|
|
253
|
+
formData.append('media[file]', file);
|
|
254
|
+
formData.append('media[filename]', file.name);
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const response = await fetch(config.uploadUrl, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: { 'X-CSRF-Token': csrfToken },
|
|
260
|
+
body: formData
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const result = await response.json();
|
|
264
|
+
|
|
265
|
+
if (response.ok && result.src) {
|
|
266
|
+
editor.AssetManager.add(result);
|
|
267
|
+
showToast(`Uploaded ${file.name}`, 'success');
|
|
268
|
+
} else if (result.errors) {
|
|
269
|
+
showToast(`Failed: ${result.errors.join(', ')}`, 'error');
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
showToast(`Error uploading ${file.name}`, 'error');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
progressContainer.style.display = 'none';
|
|
277
|
+
progressBar.style.width = '0';
|
|
278
|
+
|
|
279
|
+
// Reload media library and switch to it
|
|
280
|
+
loadMediaPage(1);
|
|
281
|
+
modal.querySelector('.ac-asset-tab[data-tab="library"]').click();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Load assets into the editor and sidebar panel
|
|
287
|
+
* @param {Object} editor - GrapeJS editor instance
|
|
288
|
+
* @param {string} mediaUrl - URL to fetch media from
|
|
289
|
+
*/
|
|
290
|
+
function loadAssets(editor, mediaUrl) {
|
|
291
|
+
const { showToast } = window.ActiveCanvasEditor;
|
|
292
|
+
|
|
293
|
+
fetch(mediaUrl, {
|
|
294
|
+
headers: { 'Accept': 'application/json' }
|
|
295
|
+
})
|
|
296
|
+
.then(response => response.json())
|
|
297
|
+
.then(result => {
|
|
298
|
+
if (result.data) {
|
|
299
|
+
editor.AssetManager.add(result.data);
|
|
300
|
+
renderAssetsPanel(result.data, editor);
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
.catch(error => {
|
|
304
|
+
console.error('Failed to load assets:', error);
|
|
305
|
+
const grid = document.getElementById('assets-grid');
|
|
306
|
+
if (grid) {
|
|
307
|
+
grid.innerHTML = '<div class="assets-empty">Failed to load assets</div>';
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Render the assets panel in the sidebar
|
|
314
|
+
*/
|
|
315
|
+
function renderAssetsPanel(assets, editor) {
|
|
316
|
+
const { showToast } = window.ActiveCanvasEditor;
|
|
317
|
+
const grid = document.getElementById('assets-grid');
|
|
318
|
+
if (!grid) return;
|
|
319
|
+
|
|
320
|
+
if (!assets || assets.length === 0) {
|
|
321
|
+
grid.innerHTML = `
|
|
322
|
+
<div class="assets-empty">
|
|
323
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
324
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
325
|
+
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
326
|
+
<polyline points="21 15 16 10 5 21"/>
|
|
327
|
+
</svg>
|
|
328
|
+
<p>No assets yet</p>
|
|
329
|
+
<span>Upload images to use them here</span>
|
|
330
|
+
</div>
|
|
331
|
+
`;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
grid.innerHTML = assets.map(asset => `
|
|
336
|
+
<div class="asset-item" draggable="true" data-src="${asset.src}" data-name="${asset.name || 'Image'}" title="${asset.name || 'Image'}">
|
|
337
|
+
<img src="${asset.src}" alt="${asset.name || 'Image'}" loading="lazy">
|
|
338
|
+
<div class="asset-item-overlay">
|
|
339
|
+
<button class="asset-insert-btn" data-src="${asset.src}" title="Insert image">
|
|
340
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
341
|
+
<line x1="12" y1="5" x2="12" y2="19"/>
|
|
342
|
+
<line x1="5" y1="12" x2="19" y2="12"/>
|
|
343
|
+
</svg>
|
|
344
|
+
</button>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
`).join('');
|
|
348
|
+
|
|
349
|
+
// Add drag and insert functionality
|
|
350
|
+
grid.querySelectorAll('.asset-item').forEach(item => {
|
|
351
|
+
// Drag to canvas
|
|
352
|
+
item.addEventListener('dragstart', (e) => {
|
|
353
|
+
const src = item.dataset.src;
|
|
354
|
+
e.dataTransfer.setData('text/html', `<img src="${src}" alt="Image" style="max-width: 100%;">`);
|
|
355
|
+
e.dataTransfer.effectAllowed = 'copy';
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Click to insert
|
|
359
|
+
const insertBtn = item.querySelector('.asset-insert-btn');
|
|
360
|
+
if (insertBtn) {
|
|
361
|
+
insertBtn.addEventListener('click', (e) => {
|
|
362
|
+
e.stopPropagation();
|
|
363
|
+
const src = insertBtn.dataset.src;
|
|
364
|
+
insertImageToCanvas(editor, src);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Double-click to insert
|
|
369
|
+
item.addEventListener('dblclick', () => {
|
|
370
|
+
const src = item.dataset.src;
|
|
371
|
+
insertImageToCanvas(editor, src);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Insert an image into the canvas
|
|
378
|
+
*/
|
|
379
|
+
function insertImageToCanvas(editor, src) {
|
|
380
|
+
const { showToast } = window.ActiveCanvasEditor;
|
|
381
|
+
const selected = editor.getSelected();
|
|
382
|
+
const wrapper = editor.getWrapper();
|
|
383
|
+
|
|
384
|
+
const imageComponent = {
|
|
385
|
+
type: 'image',
|
|
386
|
+
attributes: { src: src, alt: 'Image' },
|
|
387
|
+
style: { 'max-width': '100%' }
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (selected) {
|
|
391
|
+
const parent = selected.parent();
|
|
392
|
+
if (parent) {
|
|
393
|
+
const index = parent.components().indexOf(selected);
|
|
394
|
+
parent.components().add(imageComponent, { at: index + 1 });
|
|
395
|
+
} else {
|
|
396
|
+
wrapper.append(imageComponent);
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
wrapper.append(imageComponent);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
showToast('Image inserted', 'success');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Setup the assets panel controls (upload, refresh)
|
|
407
|
+
*/
|
|
408
|
+
function setupAssetsPanel(editor, config, csrfToken) {
|
|
409
|
+
const { showToast } = window.ActiveCanvasEditor;
|
|
410
|
+
const uploadBtn = document.getElementById('btn-upload-asset');
|
|
411
|
+
const refreshBtn = document.getElementById('btn-refresh-assets');
|
|
412
|
+
const uploadInput = document.getElementById('asset-upload-input');
|
|
413
|
+
|
|
414
|
+
if (!uploadBtn || !refreshBtn || !uploadInput) return;
|
|
415
|
+
|
|
416
|
+
// Upload button click
|
|
417
|
+
uploadBtn.addEventListener('click', () => {
|
|
418
|
+
uploadInput.click();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Refresh button click
|
|
422
|
+
refreshBtn.addEventListener('click', () => {
|
|
423
|
+
const grid = document.getElementById('assets-grid');
|
|
424
|
+
if (grid) {
|
|
425
|
+
grid.innerHTML = '<div class="assets-loading">Loading assets...</div>';
|
|
426
|
+
}
|
|
427
|
+
loadAssets(editor, config.mediaUrl);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Handle file selection
|
|
431
|
+
uploadInput.addEventListener('change', async (e) => {
|
|
432
|
+
const files = e.target.files;
|
|
433
|
+
if (!files || files.length === 0) return;
|
|
434
|
+
|
|
435
|
+
uploadBtn.disabled = true;
|
|
436
|
+
uploadBtn.innerHTML = `
|
|
437
|
+
<svg class="spin" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
438
|
+
<line x1="12" y1="2" x2="12" y2="6"/>
|
|
439
|
+
<line x1="12" y1="18" x2="12" y2="22"/>
|
|
440
|
+
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/>
|
|
441
|
+
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/>
|
|
442
|
+
<line x1="2" y1="12" x2="6" y2="12"/>
|
|
443
|
+
<line x1="18" y1="12" x2="22" y2="12"/>
|
|
444
|
+
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/>
|
|
445
|
+
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/>
|
|
446
|
+
</svg>
|
|
447
|
+
Uploading...
|
|
448
|
+
`;
|
|
449
|
+
|
|
450
|
+
for (const file of files) {
|
|
451
|
+
const formData = new FormData();
|
|
452
|
+
formData.append('media[file]', file);
|
|
453
|
+
formData.append('media[filename]', file.name);
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const response = await fetch(config.uploadUrl, {
|
|
457
|
+
method: 'POST',
|
|
458
|
+
headers: { 'X-CSRF-Token': csrfToken },
|
|
459
|
+
body: formData
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const result = await response.json();
|
|
463
|
+
|
|
464
|
+
if (response.ok && result.src) {
|
|
465
|
+
editor.AssetManager.add(result);
|
|
466
|
+
showToast(`Uploaded ${file.name}`, 'success');
|
|
467
|
+
} else if (result.errors) {
|
|
468
|
+
showToast(`Failed: ${result.errors.join(', ')}`, 'error');
|
|
469
|
+
}
|
|
470
|
+
} catch (error) {
|
|
471
|
+
showToast(`Error uploading ${file.name}`, 'error');
|
|
472
|
+
console.error('Upload error:', error);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Reset and refresh
|
|
477
|
+
uploadInput.value = '';
|
|
478
|
+
uploadBtn.disabled = false;
|
|
479
|
+
uploadBtn.innerHTML = `
|
|
480
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
481
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
482
|
+
<polyline points="17 8 12 3 7 8"/>
|
|
483
|
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
484
|
+
</svg>
|
|
485
|
+
Upload
|
|
486
|
+
`;
|
|
487
|
+
|
|
488
|
+
// Refresh assets panel
|
|
489
|
+
loadAssets(editor, config.mediaUrl);
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Expose functions
|
|
494
|
+
window.ActiveCanvasEditor.setupCustomAssetManager = setupCustomAssetManager;
|
|
495
|
+
window.ActiveCanvasEditor.loadAssets = loadAssets;
|
|
496
|
+
window.ActiveCanvasEditor.setupAssetsPanel = setupAssetsPanel;
|
|
497
|
+
|
|
498
|
+
})();
|