Skip to content

Commit 61fa769

Browse files
committed
feat: add image field type for Simple Blocks with Media Library integration
- Added 'image' field type to Custom Blocks - Image fields open Strapi Media Library for selection - Shows image preview with Select/Remove buttons - Stores url, alt, width, height, id, documentId - Seamless integration via CustomEvent bridge
1 parent 9a9e579 commit 61fa769

3 files changed

Lines changed: 219 additions & 2 deletions

File tree

admin/src/components/EditorJS/index.jsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3265,6 +3265,44 @@ const Editor = forwardRef(({
32653265
[mediaLibBlockIndex, mediaLibToggleFunc]
32663266
);
32673267

3268+
// Custom Block Image Field - Media Library integration
3269+
const [customBlockImageCallback, setCustomBlockImageCallback] = useState(null);
3270+
3271+
const handleCustomBlockMediaLibChange = useCallback((data) => {
3272+
if (customBlockImageCallback && window.__MAGIC_EDITOR_IMAGE_CALLBACKS__) {
3273+
const callback = window.__MAGIC_EDITOR_IMAGE_CALLBACKS__[customBlockImageCallback];
3274+
if (callback) {
3275+
// Format the data for the image field
3276+
const formattedData = data.map((file) => ({
3277+
url: file.url,
3278+
alt: file.alt || file.name || '',
3279+
width: file.width,
3280+
height: file.height,
3281+
id: file.id,
3282+
documentId: file.documentId,
3283+
name: file.name,
3284+
}));
3285+
callback(formattedData);
3286+
}
3287+
}
3288+
setCustomBlockImageCallback(null);
3289+
setIsMediaLibOpen(false);
3290+
}, [customBlockImageCallback]);
3291+
3292+
// Listen for custom block image field events
3293+
useEffect(() => {
3294+
const handleOpenMediaLib = (event) => {
3295+
const { callbackId, allowedTypes } = event.detail || {};
3296+
if (callbackId) {
3297+
setCustomBlockImageCallback(callbackId);
3298+
setIsMediaLibOpen(true);
3299+
}
3300+
};
3301+
3302+
window.addEventListener('magic-editor-open-media-lib', handleOpenMediaLib);
3303+
return () => window.removeEventListener('magic-editor-open-media-lib', handleOpenMediaLib);
3304+
}, []);
3305+
32683306
// Fullscreen toggle
32693307
const toggleFullscreen = useCallback(() => {
32703308
setIsFullscreen(prev => {
@@ -4077,8 +4115,14 @@ const Editor = forwardRef(({
40774115

40784116
<MediaLibComponent
40794117
isOpen={isMediaLibOpen}
4080-
onChange={handleMediaLibChange}
4081-
onToggle={mediaLibToggleFunc}
4118+
onChange={customBlockImageCallback ? handleCustomBlockMediaLibChange : handleMediaLibChange}
4119+
onToggle={() => {
4120+
if (customBlockImageCallback) {
4121+
setCustomBlockImageCallback(null);
4122+
}
4123+
mediaLibToggleFunc();
4124+
}}
4125+
multiple={!customBlockImageCallback}
40824126
/>
40834127

40844128
{/* AI Inline Toolbar - New Smart UI */}

admin/src/pages/CustomBlocksPage.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,6 +1758,7 @@ const CustomBlocksPage = () => {
17581758
<option value="select">Select</option>
17591759
<option value="color">Color</option>
17601760
<option value="checkbox">Checkbox</option>
1761+
<option value="image">Image</option>
17611762
</FieldSelect>
17621763
<IconButton $danger onClick={() => removeField(index)}>
17631764
<TrashIcon />

admin/src/utils/blockFactory.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ export function createSimpleBlock(config) {
349349
});
350350
break;
351351

352+
case 'image':
353+
input = this._createImageField(field);
354+
break;
355+
352356
default: // text input
353357
input = document.createElement('input');
354358
input.type = field.type || 'text';
@@ -374,6 +378,174 @@ export function createSimpleBlock(config) {
374378
return container;
375379
}
376380

381+
/**
382+
* Create image field with Media Library integration
383+
* @param {object} field - Field configuration
384+
* @returns {HTMLElement}
385+
*/
386+
_createImageField(field) {
387+
const wrapper = document.createElement('div');
388+
wrapper.style.cssText = `
389+
border: 2px dashed #e2e8f0;
390+
border-radius: 8px;
391+
padding: 16px;
392+
text-align: center;
393+
background: #fafbfc;
394+
transition: all 0.2s ease;
395+
`;
396+
397+
// Image preview
398+
const preview = document.createElement('div');
399+
preview.className = 'image-preview';
400+
preview.style.cssText = `
401+
margin-bottom: 12px;
402+
min-height: 60px;
403+
display: flex;
404+
align-items: center;
405+
justify-content: center;
406+
`;
407+
408+
const img = document.createElement('img');
409+
img.style.cssText = `
410+
max-width: 100%;
411+
max-height: 150px;
412+
border-radius: 6px;
413+
display: none;
414+
`;
415+
416+
const placeholder = document.createElement('div');
417+
placeholder.innerHTML = `
418+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="1.5">
419+
<rect x="3" y="3" width="18" height="18" rx="2"/>
420+
<circle cx="8.5" cy="8.5" r="1.5"/>
421+
<path d="M21 15l-5-5L5 21"/>
422+
</svg>
423+
<p style="margin: 8px 0 0; color: #94a3b8; font-size: 13px;">No image selected</p>
424+
`;
425+
placeholder.style.cssText = 'text-align: center;';
426+
427+
preview.appendChild(img);
428+
preview.appendChild(placeholder);
429+
430+
// Update preview based on current data
431+
const updatePreview = (url) => {
432+
if (url) {
433+
img.src = url;
434+
img.style.display = 'block';
435+
placeholder.style.display = 'none';
436+
wrapper.style.borderStyle = 'solid';
437+
wrapper.style.borderColor = '#c4b5fd';
438+
} else {
439+
img.style.display = 'none';
440+
placeholder.style.display = 'block';
441+
wrapper.style.borderStyle = 'dashed';
442+
wrapper.style.borderColor = '#e2e8f0';
443+
}
444+
};
445+
446+
// Initial state
447+
const currentValue = this.data[field.name];
448+
if (currentValue && typeof currentValue === 'object') {
449+
updatePreview(currentValue.url);
450+
} else if (typeof currentValue === 'string' && currentValue) {
451+
updatePreview(currentValue);
452+
}
453+
454+
// Buttons container
455+
const buttons = document.createElement('div');
456+
buttons.style.cssText = 'display: flex; gap: 8px; justify-content: center;';
457+
458+
// Select button
459+
const selectBtn = document.createElement('button');
460+
selectBtn.type = 'button';
461+
selectBtn.textContent = 'Select Image';
462+
selectBtn.style.cssText = `
463+
padding: 8px 16px;
464+
background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
465+
color: white;
466+
border: none;
467+
border-radius: 6px;
468+
font-size: 13px;
469+
font-weight: 600;
470+
cursor: pointer;
471+
transition: all 0.2s ease;
472+
`;
473+
selectBtn.addEventListener('mouseenter', () => {
474+
selectBtn.style.transform = 'translateY(-1px)';
475+
selectBtn.style.boxShadow = '0 4px 12px rgba(124, 58, 237, 0.35)';
476+
});
477+
selectBtn.addEventListener('mouseleave', () => {
478+
selectBtn.style.transform = 'translateY(0)';
479+
selectBtn.style.boxShadow = 'none';
480+
});
481+
482+
// Remove button
483+
const removeBtn = document.createElement('button');
484+
removeBtn.type = 'button';
485+
removeBtn.textContent = 'Remove';
486+
removeBtn.style.cssText = `
487+
padding: 8px 16px;
488+
background: transparent;
489+
color: #ef4444;
490+
border: 1px solid #fecaca;
491+
border-radius: 6px;
492+
font-size: 13px;
493+
font-weight: 600;
494+
cursor: pointer;
495+
transition: all 0.2s ease;
496+
display: ${this.data[field.name] ? 'inline-block' : 'none'};
497+
`;
498+
removeBtn.addEventListener('click', () => {
499+
this.data[field.name] = null;
500+
updatePreview(null);
501+
removeBtn.style.display = 'none';
502+
});
503+
504+
// Handle select button click - dispatch custom event
505+
selectBtn.addEventListener('click', () => {
506+
if (this.readOnly) return;
507+
508+
// Create a unique callback ID
509+
const callbackId = `image_field_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
510+
511+
// Store callback globally for Media Library to call
512+
window.__MAGIC_EDITOR_IMAGE_CALLBACKS__ = window.__MAGIC_EDITOR_IMAGE_CALLBACKS__ || {};
513+
window.__MAGIC_EDITOR_IMAGE_CALLBACKS__[callbackId] = (files) => {
514+
if (files && files.length > 0) {
515+
const file = files[0];
516+
const imageData = {
517+
url: file.url,
518+
alt: file.alt || file.name || '',
519+
width: file.width,
520+
height: file.height,
521+
id: file.id,
522+
documentId: file.documentId,
523+
};
524+
this.data[field.name] = imageData;
525+
updatePreview(imageData.url);
526+
removeBtn.style.display = 'inline-block';
527+
}
528+
// Cleanup
529+
delete window.__MAGIC_EDITOR_IMAGE_CALLBACKS__[callbackId];
530+
};
531+
532+
// Dispatch event to open Media Library
533+
window.dispatchEvent(new CustomEvent('magic-editor-open-media-lib', {
534+
detail: { callbackId, fieldName: field.name, allowedTypes: ['images'] }
535+
}));
536+
});
537+
538+
buttons.appendChild(selectBtn);
539+
buttons.appendChild(removeBtn);
540+
541+
wrapper.appendChild(preview);
542+
if (!this.readOnly) {
543+
wrapper.appendChild(buttons);
544+
}
545+
546+
return wrapper;
547+
}
548+
377549
/**
378550
* Escape HTML special characters
379551
* @param {string} text - Text to escape

0 commit comments

Comments
 (0)