£Á°èZ¨Ä…–K§‚«“ô4“ÒÙ´dîfUÙÃÅ WKbyʦ•ꎅȮFÒ¿ÊÎóCozá¬S@6{Í:›œêZÌ:Š•_%:¢¾¾~;‘Ã~芩ÊǍí`ÔÑ©ú뙵'5I¿fš×WO%ø9¾«¾DK|€ùÍD”Ýs]nHÕ¶êםӼ㞪éUWŸÈË%DÒÕ¬ï‘]/Åcx ‰ï2ß]ä6G[]S£Ôϯrs{úëóµmÒï#UQxo·õÞCe]"±/aÙ&Eã4ú9Jé_ÞåëdãöKë)AÞ ¯¹ægƒÛowЍø^d™ý½ßB7áyMä9ÜÖUã !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! custom-slider-block-simple/ ├── custom-slider-block-simple.php ├── editor.js ├── editor.css ├── frontend.js └── style.css 1) custom-slider-block-simple.php php Copy code 'csbs-editor-js', 'editor_style' => 'csbs-editor-css', 'style' => 'csbs-style-css', ) ); } add_action( 'init', 'csbs_register_block_assets' ); /** * Enqueue Swiper + frontend init only if block exists on the page. */ function csbs_enqueue_frontend_assets() { if ( ! function_exists( 'has_block' ) ) { return; } if ( has_block( 'hiral/simple-slider' ) ) { // Swiper CDN (CSS + JS) wp_enqueue_style( 'swiper-cdn-css', 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css', array(), null ); wp_enqueue_script( 'swiper-cdn-js', 'https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js', array(), null, true ); // Our frontend initializer (depends on Swiper loaded on page) wp_enqueue_script( 'csbs-frontend-js' ); // Ensure our block CSS is printed (registered earlier as 'csbs-style-css' via register_block_type -> style) wp_enqueue_style( 'csbs-style-css' ); } } add_action( 'wp_enqueue_scripts', 'csbs_enqueue_frontend_assets' ); 2) editor.js javascript Copy code ( function (wp) { const { registerBlockType } = wp.blocks; const { MediaUpload, MediaUploadCheck, InspectorControls } = wp.blockEditor || wp.editor; const { Button, PanelBody, TextControl } = wp.components; const { createElement: el, Fragment } = wp.element; registerBlockType('hiral/simple-slider', { title: 'Simple Slider (with captions)', icon: 'images-alt2', category: 'widgets', attributes: { images: { type: 'array', default: [] } }, edit: function (props) { const { attributes: { images = [] }, setAttributes } = props; function onSelectImages(selection) { let imgs = []; if ( Array.isArray(selection) ) { imgs = selection.map(i => ({ id: i.id, url: i.url || (i.sizes && i.sizes.full && i.sizes.full.url) || i.source_url || '', caption: i.caption || '' })); } else if ( selection && typeof selection === 'object' ) { imgs = [{ id: selection.id, url: selection.url || (selection.sizes && selection.sizes.full && selection.sizes.full.url) || selection.source_url || '', caption: selection.caption || '' }]; } else { imgs = []; } setAttributes({ images: imgs }); } function updateCaption(id, value) { const updated = images.map(img => { if ( img.id === id ) { return Object.assign({}, img, { caption: value }); } return img; }); setAttributes({ images: updated }); } function removeImage(id) { const filtered = images.filter(img => img.id !== id); setAttributes({ images: filtered }); } function moveUp(index) { if (index === 0) return; const arr = images.slice(); const tmp = arr[index - 1]; arr[index - 1] = arr[index]; arr[index] = tmp; setAttributes({ images: arr }); } function moveDown(index) { if (index === images.length - 1) return; const arr = images.slice(); const tmp = arr[index + 1]; arr[index + 1] = arr[index]; arr[index] = tmp; setAttributes({ images: arr }); } return el( Fragment, {}, el( InspectorControls, {}, el( PanelBody, { title: 'Slider Images & Captions', initialOpen: true }, el( MediaUploadCheck, {}, el( MediaUpload, { onSelect: onSelectImages, allowedTypes: ['image'], multiple: true, gallery: true, value: images.map(i => i.id), render: function ( obj ) { return el( Button, { isPrimary: true, onClick: obj.open }, 'Select / Replace Images' ); } } ) ), el('p', { style: { marginTop: '8px', color: '#666' } }, 'After selecting images, use the fields in the editor preview to add captions.') ) ), el( 'div', { className: 'csbs-editor-wrapper' }, images.length === 0 ? el('p', {}, 'No images selected — open Block settings (Inspector) and click "Select / Replace Images".') : images.map(function (img, idx) { return el( 'div', { key: img.id, style: { marginBottom: '16px', border: '1px solid #eee', padding: '8px', borderRadius: '6px' } }, el('img', { src: img.url, style: { width: '100%', display: 'block', marginBottom: '8px' } }), el(TextControl, { label: 'Caption (overlay bottom)', value: img.caption || '', onChange: function (val) { updateCaption(img.id, val); } }), el('div', { style: { marginTop: '8px', display: 'flex', gap: '8px' } }, el(Button, { isSecondary: true, onClick: function () { moveUp(idx); }, disabled: idx === 0 }, '↑'), el(Button, { isSecondary: true, onClick: function () { moveDown(idx); }, disabled: idx === images.length - 1 }, '↓'), el(Button, { isDestructive: true, onClick: function () { removeImage(img.id); } }, 'Remove') ) ); }) ) ); }, save: function (props) { const images = ( props.attributes && props.attributes.images ) || []; return el( 'div', { className: 'swiper csbs-slider' }, el( 'div', { className: 'swiper-wrapper' }, images.map(function (img) { return el( 'div', { className: 'swiper-slide csbs-slide', key: img.id }, el('img', { src: img.url, alt: (img.caption || '') }), img.caption ? el('div', { className: 'csbs-caption' }, img.caption) : null ); }) ), el('div', { className: 'swiper-button-prev' }), el('div', { className: 'swiper-button-next' }), el('div', { className: 'swiper-pagination' }) ); } } ); } )( window.wp ); 3) editor.css css Copy code .csbs-editor-wrapper img { border: 1px solid #ddd; border-radius: 4px; } .csbs-editor-wrapper .components-text-control__input { width: 100%; } 4) frontend.js javascript Copy code (function () { function initSliders() { if ( typeof Swiper === 'undefined' ) { return; } const sliders = document.querySelectorAll('.csbs-slider'); sliders.forEach(function (slider) { // initialize swiper with container-scoped selectors new Swiper(slider, { loop: true, pagination: { el: slider.querySelector('.swiper-pagination'), clickable: true }, navigation: { nextEl: slider.querySelector('.swiper-button-next'), prevEl: slider.querySelector('.swiper-button-prev') }, autoplay: { delay: 4000, disableOnInteraction: false }, slidesPerView: 1 }); }); } // Init when DOM ready or when Swiper arrives function readyInit() { initSliders(); // retry if Swiper loads afterwards let attempts = 0; const interval = setInterval(function () { attempts++; if ( typeof Swiper !== 'undefined' ) { initSliders(); clearInterval(interval); } if ( attempts > 10 ) clearInterval(interval); }, 400); } if ( document.readyState === 'loading' ) { document.addEventListener('DOMContentLoaded', readyInit); } else { readyInit(); } })(); 5) style.css css Copy code /* Basic slide image */ .csbs-slider img { width: 100%; display: block; object-fit: cover; } /* ensure slides have some height (you can adjust) */ .csbs-slider .swiper-slide { position: relative; min-height: 220px; } /* caption overlay: dark translucent background at bottom */ .csbs-caption { position: absolute; left: 0; right: 0; bottom: 0; padding: 12px 16px; background: rgba(0,0,0,0.55); color: #fff; font-size: 15px; line-height: 1.3; box-sizing: border-box; max-height: 40%; overflow: hidden; } /* make sure nav has above caption */ .csbs-slider .swiper-button-next, .csbs-slider .swiper-button-prev { z-index: 20; } /* pagination above caption */ .csbs-slider .swiper-pagination { z-index: 20; } /* small responsive tweak */ @media (max-width: 600px) { .csbs-caption { font-size: 14px; padding: 10px; } .csbs-slider .swiper-slide { min-height: 160px; } }