| New file |
| | |
| | | //edit.js |
| | | import { __ } from '@wordpress/i18n'; |
| | | import { |
| | | useBlockProps, |
| | | InspectorControls, |
| | | MediaUpload, |
| | | MediaUploadCheck, |
| | | InnerBlocks, |
| | | useInnerBlocksProps |
| | | } from '@wordpress/block-editor'; |
| | | import { |
| | | PanelBody, |
| | | Button, |
| | | ToggleControl, |
| | | BaseControl, |
| | | RangeControl, |
| | | SelectControl |
| | | } from '@wordpress/components'; |
| | | import './editor.scss'; |
| | | |
| | | const ALLOWED_VIDEO_TYPES = ['video']; |
| | | |
| | | const INNER_BLOCKS_TEMPLATE = [ |
| | | ['core/heading', { |
| | | level: 1, |
| | | placeholder: 'Add heading...', |
| | | textAlign: 'center' |
| | | }], |
| | | ['core/paragraph', { |
| | | placeholder: 'Add description...', |
| | | align: 'center' |
| | | }], |
| | | ['core/buttons', { |
| | | layout: { type: 'flex', justifyContent: 'center' } |
| | | }] |
| | | ]; |
| | | |
| | | |
| | | |
| | | export default function Edit({ attributes, setAttributes }) { |
| | | const { |
| | | posterId, |
| | | posterUrl, |
| | | videoSources, |
| | | fadeEffect, |
| | | overlayOpacity, |
| | | contentAlignment, |
| | | minHeight |
| | | } = attributes; |
| | | |
| | | const blockProps = useBlockProps({ |
| | | className: 'video-cover-editor', |
| | | style: { |
| | | minHeight: minHeight ? `${minHeight}px` : undefined |
| | | } |
| | | }); |
| | | |
| | | const innerBlocksProps = useInnerBlocksProps( |
| | | { className: 'video-cover-content' }, |
| | | { |
| | | template: INNER_BLOCKS_TEMPLATE, |
| | | templateLock: false |
| | | } |
| | | ); |
| | | |
| | | const onSelectPoster = (media) => { |
| | | setAttributes({ |
| | | posterId: media.id, |
| | | posterUrl: media.url |
| | | }); |
| | | }; |
| | | |
| | | const onSelectVideos = (mediaItems) => { |
| | | // multiple=true returns an array |
| | | const items = Array.isArray(mediaItems) ? mediaItems : [mediaItems]; |
| | | const newSources = items |
| | | .filter(media => !videoSources.some(s => s.id === media.id)) |
| | | .map(media => ({ |
| | | id: media.id, |
| | | url: media.url, |
| | | mime: media.mime |
| | | })); |
| | | |
| | | if (newSources.length) { |
| | | setAttributes({ |
| | | videoSources: [...videoSources, ...newSources] |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const removeVideoSource = (index) => { |
| | | const updated = [...videoSources]; |
| | | updated.splice(index, 1); |
| | | setAttributes({ videoSources: updated }); |
| | | }; |
| | | |
| | | const renderVideoSourceList = (sources, isMobile = false) => { |
| | | if (sources.length === 0) return null; |
| | | |
| | | return ( |
| | | <ul className="video-source-list"> |
| | | {sources.map((source, index) => ( |
| | | <li key={index} className="video-source-item"> |
| | | <span className="video-source-mime">{source.mime}</span> |
| | | <Button |
| | | isDestructive |
| | | isSmall |
| | | onClick={() => removeVideoSource(index, isMobile)} |
| | | > |
| | | {__('Remove', 'jvb')} |
| | | </Button> |
| | | </li> |
| | | ))} |
| | | </ul> |
| | | ); |
| | | }; |
| | | |
| | | return ( |
| | | <> |
| | | <InspectorControls> |
| | | <PanelBody title={__('Video Settings', 'jvb')} initialOpen={true}> |
| | | <BaseControl |
| | | label={__('Poster Image', 'jvb')} |
| | | help={__('Image shown while video loads', 'jvb')} |
| | | > |
| | | <MediaUploadCheck> |
| | | <MediaUpload |
| | | onSelect={onSelectPoster} |
| | | allowedTypes={['image']} |
| | | value={posterId} |
| | | render={({ open }) => ( |
| | | <> |
| | | {posterUrl && ( |
| | | <img |
| | | src={posterUrl} |
| | | alt={__('Poster preview', 'jvb')} |
| | | style={{ maxWidth: '100%', marginBottom: '10px' }} |
| | | /> |
| | | )} |
| | | <Button |
| | | onClick={open} |
| | | variant={posterUrl ? 'secondary' : 'primary'} |
| | | > |
| | | {posterUrl |
| | | ? __('Change Poster', 'jvb') |
| | | : __('Select Poster', 'jvb')} |
| | | </Button> |
| | | {posterUrl && ( |
| | | <Button |
| | | isDestructive |
| | | onClick={() => setAttributes({ posterId: 0, posterUrl: '' })} |
| | | style={{ marginLeft: '10px' }} |
| | | > |
| | | {__('Remove', 'jvb')} |
| | | </Button> |
| | | )} |
| | | </> |
| | | )} |
| | | /> |
| | | </MediaUploadCheck> |
| | | </BaseControl> |
| | | |
| | | <BaseControl |
| | | label={__('Video Sources', 'jvb')} |
| | | help={__('Add multiple formats for better browser support (mp4, webm, etc.)', 'jvb')} |
| | | > |
| | | {videoSources.length > 0 && ( |
| | | <ul className="video-source-list"> |
| | | {videoSources.map((source, index) => ( |
| | | <li key={index} className="video-source-item"> |
| | | <span className="video-source-mime">{source.mime}</span> |
| | | <Button |
| | | isDestructive |
| | | isSmall |
| | | onClick={() => removeVideoSource(index)} |
| | | > |
| | | {__('Remove', 'jvb')} |
| | | </Button> |
| | | </li> |
| | | ))} |
| | | </ul> |
| | | )} |
| | | <MediaUploadCheck> |
| | | <MediaUpload |
| | | multiple={true} |
| | | onSelect={onSelectVideos} |
| | | allowedTypes={ALLOWED_VIDEO_TYPES} |
| | | render={({ open }) => ( |
| | | <Button onClick={open} variant="secondary"> |
| | | {__('Add Video', 'jvb')} |
| | | </Button> |
| | | )} |
| | | /> |
| | | </MediaUploadCheck> |
| | | </BaseControl> |
| | | |
| | | <ToggleControl |
| | | label={__('Fade Effect', 'jvb')} |
| | | help={__('Add fade class to video element', 'jvb')} |
| | | checked={fadeEffect} |
| | | onChange={(value) => setAttributes({ fadeEffect: value })} |
| | | /> |
| | | </PanelBody> |
| | | |
| | | <PanelBody title={__('Overlay Settings', 'jvb')} initialOpen={true}> |
| | | <RangeControl |
| | | label={__('Overlay Opacity', 'jvb')} |
| | | help={__('Darken video for better text readability', 'jvb')} |
| | | value={overlayOpacity} |
| | | onChange={(value) => setAttributes({ overlayOpacity: value })} |
| | | min={0} |
| | | max={100} |
| | | step={5} |
| | | /> |
| | | |
| | | <SelectControl |
| | | label={__('Content Alignment', 'jvb')} |
| | | value={contentAlignment} |
| | | options={[ |
| | | { label: __('Top Left', 'jvb'), value: 'top-left' }, |
| | | { label: __('Top Center', 'jvb'), value: 'top-center' }, |
| | | { label: __('Top Right', 'jvb'), value: 'top-right' }, |
| | | { label: __('Center Left', 'jvb'), value: 'center-left' }, |
| | | { label: __('Center', 'jvb'), value: 'center' }, |
| | | { label: __('Center Right', 'jvb'), value: 'center-right' }, |
| | | { label: __('Bottom Left', 'jvb'), value: 'bottom-left' }, |
| | | { label: __('Bottom Center', 'jvb'), value: 'bottom-center' }, |
| | | { label: __('Bottom Right', 'jvb'), value: 'bottom-right' } |
| | | ]} |
| | | onChange={(value) => setAttributes({ contentAlignment: value })} |
| | | /> |
| | | |
| | | <RangeControl |
| | | label={__('Minimum Height', 'jvb')} |
| | | help={__('Minimum height in pixels (leave 0 for auto)', 'jvb')} |
| | | value={minHeight} |
| | | onChange={(value) => setAttributes({ minHeight: value })} |
| | | min={0} |
| | | max={1000} |
| | | step={50} |
| | | /> |
| | | </PanelBody> |
| | | </InspectorControls> |
| | | |
| | | <div {...blockProps}> |
| | | {posterUrl || videoSources.length > 0 ? ( |
| | | <div className="video-cover-preview"> |
| | | {posterUrl && ( |
| | | <> |
| | | <img src={posterUrl} alt={__('Video poster', 'jvb')} /> |
| | | {overlayOpacity > 0 && ( |
| | | <div |
| | | className="video-overlay-preview" |
| | | style={{ opacity: overlayOpacity / 100 }} |
| | | /> |
| | | )} |
| | | </> |
| | | )} |
| | | <div className={`video-cover-content-preview align-${contentAlignment}`}> |
| | | <div {...innerBlocksProps} /> |
| | | </div> |
| | | <div className="video-info"> |
| | | <p> |
| | | {videoSources.length} {__('desktop source(s)', 'jvb')} |
| | | </p> |
| | | </div> |
| | | </div> |
| | | ) : ( |
| | | <div className="video-cover-placeholder"> |
| | | <p>{__('Configure video sources in the sidebar →', 'jvb')}</p> |
| | | </div> |
| | | )} |
| | | </div> |
| | | </> |
| | | ); |
| | | } |