import * as React from 'react' import {__, _x, sprintf} from '@wordpress/i18n' import cx from 'classnames' import { RangeControl, ToggleControl, FocalPointPicker, MenuItem, } from '@wordpress/components' import { hasBlockSupport, // @ts-expect-error } from '@wordpress/blocks' import { addFilter, } from '@wordpress/hooks' import { useSelect, } from '@wordpress/data' import { store as coreDataStore, type Attachment, } from '@wordpress/core-data' import { BlockControls, InspectorControls, MediaReplaceFlow, // @ts-expect-error } from '@wordpress/block-editor' import { createHigherOrderComponent, } from '@wordpress/compose' import { siteLogo as siteLogoIcon, trash as trashIcon, } from '@wordpress/icons' import { ButtonGroup, WithTooltip, } from '@ska/components' import { SkaPanelBody, } from '@ska/plugin' import { ImageSizeControl, PlaceholderImageControl, } from '../components' import { useSkaBlocksDispatch, } from '../tailwind/reducer' import { homeUrl, placeholderImageUrl, } from '../data' import { selectImageUrl, type tImage, type tBlockEditProps, type tBlockNameOrType, type tBlockSaveProps, type tBlockType, type tBlockEditInnerBlocksProps, type tBlockSaveInnerBlocksProps, type tVideo, type tBlockAttributes, getPlaceholderSvg, } from '@ska/shared' export interface BackgroundValue { mode?: 'image' | 'video' | 'background' | 'placeholder' id?: number src?: string posterId?: number posterSrc?: string loading?: 'lazy' | 'eager' | 'lcp' | 'preload' /** Add `srcset` to image when possible. */ srcset?: boolean /** Add low res placeholder background to the image. */ placeholder?: boolean /** Whether to preload low res placeholder in head. */ preloadPlaceholder?: boolean /** Whether to preload poster image in head. */ preloadPoster?: boolean size?: 'cover' | 'contain' | 'fill' | 'scale-down' repeat?: string opacity?: number focalPoint?: {x: number, y: number} placeholderIndex?: number /** Whether to autoplay in editor. */ play?: boolean } export interface BackgroundAttributes { skaBlocksBackground?: BackgroundValue } export const BACKGROUND_SUPPORT_KEY = 'skaBlocksBackground' export const BACKGROUND_SUPPORT_DEFAULT_ENABLED = false export const hasBackgroundSupport = (block: tBlockNameOrType) => hasBlockSupport(block, BACKGROUND_SUPPORT_KEY, BACKGROUND_SUPPORT_DEFAULT_ENABLED) const PLACEHOLDER_SVG = getPlaceholderSvg() const PLACEHOLDER_SRC = `data:image/svg+xml;base64,${btoa(PLACEHOLDER_SVG.trim())}` const PLACEHOLDER_SRC_PLACEHOLDER = '%SKA_PLACEHOLDER_IMAGE%' const BACKGROUND_PREFIX = '🌄 ' const getBlockLabelRenderer = (defaultLabel: string, __experimentalLabel?: tBlockType['__experimentalLabel']) => { const getLabel: tBlockType['__experimentalLabel'] = (attributes, arg) => { const {context} = arg const label = __experimentalLabel ? (__experimentalLabel(attributes, arg) || defaultLabel) : defaultLabel if(context !== 'list-view' || typeof label !== 'string') { return label } const { skaBlocksBackground = {}, } = attributes const { src, } = skaBlocksBackground return `${src ? BACKGROUND_PREFIX : ''}${label}` } return getLabel } const withBackgroundSupport = (settings: tBlockType) => { const hasSupport = hasBackgroundSupport(settings) if(!hasSupport) { switch(settings.name) { /** * Support adding a background to embed - it's only used by `ska-theme` as a placeholder when using `Video` preset. */ case 'core/embed': if(settings.supports) { Object.assign(settings.supports, {[BACKGROUND_SUPPORT_KEY]: true}) } else { settings.supports = {[BACKGROUND_SUPPORT_KEY]: true} } break default: return settings } } if(!settings.attributes.skaBlocksBackground) { Object.assign(settings.attributes, { skaBlocksBackground: { type: 'object', }, }) } if(hasSupport) { settings.__experimentalLabel = getBlockLabelRenderer(settings.title, settings.__experimentalLabel) } return settings } const MODES = [ { label: _x('Image', 'Background mode', 'ska-blocks'), value: 'image', }, { label: _x('Video', 'Background mode', 'ska-blocks'), value: 'video', }, { label: _x('Background', 'Background mode', 'ska-blocks'), value: 'background', }, { label: _x('Placeholder', 'Background mode', 'ska-blocks'), value: 'placeholder', }, ] const LOADING = [ { label: _x('Lazy', 'Image loading attribute', 'ska-blocks'), tooltip: _x(`Lazyload the image.`, 'Image loading description', 'ska-blocks'), value: 'lazy', }, { label: _x('Eager', 'Image loading attribute', 'ska-blocks'), tooltip: _x(`Don't lazyload the image.`, 'Image loading description', 'ska-blocks'), value: 'eager', }, { label: _x('LCP', 'Image loading attribute', 'ska-blocks'), tooltip: _x(`Enable high fetchpriority and synchronous decoding.`, 'Image loading description', 'ska-blocks'), value: 'lcp', }, { label: _x('Preload', 'Image loading attribute', 'ska-blocks'), tooltip: _x(`Preload the image in document head.`, 'Image loading description', 'ska-blocks'), value: 'preload', }, ] const BACKGROUND_SIZE = [ { label: __('Cover', 'ska-blocks'), value: 'cover', }, { label: __('Contain', 'ska-blocks'), value: 'contain', }, { label: __('Fill', 'ska-blocks'), value: 'fill', }, { label: __('Scale down', 'ska-blocks'), value: 'scale-down', }, ] const VALID_BACKGROUND_SIZES = ['cover', 'contain'] const BACKGROUND_REPEAT = [ { label: __('No repeat', 'ska-blocks'), value: 'no-repeat', }, { label: __('Repeat', 'ska-blocks'), value: 'repeat', }, { label: __('Repeat X', 'ska-blocks'), value: 'repeat-x', }, { label: __('Repeat Y', 'ska-blocks'), value: 'repeat-y', }, { label: __('Space', 'ska-blocks'), value: 'space', }, { label: __('Round', 'ska-blocks'), value: 'round', }, ] const DEFAULT_FOCAL_POINT = {x: 0.5, y: 0.5} const mediaPosition = ({x, y} = DEFAULT_FOCAL_POINT) => { return `${Math.round(x * 100)}% ${Math.round(y * 100)}%` } const VIDEO_EXTENSIONS = ['.mp4', '.mkv', '.webm', '.mov', '.avi', '.wmv', '.flv', '.mpeg', '.mpg'] const isVideoUrl = (url: string) => { const cleanUrl = url.trim().split(/[#?]/)[0].toLowerCase() return VIDEO_EXTENSIONS.some(ext => cleanUrl.endsWith(ext)) } const BackgroundInspectorControls: React.FC> = (props) => { const { name, attributes, setAttributes, } = props const { skaBlocksBackground = {}, } = attributes const { mode = 'image', id, src, posterId, posterSrc, loading = 'lazy', srcset = true, placeholder = false, preloadPlaceholder = true, preloadPoster = false, size = BACKGROUND_SIZE[0].value, repeat = BACKGROUND_REPEAT[0].value, opacity = 1, focalPoint = DEFAULT_FOCAL_POINT, placeholderIndex = 0, play = true, } = skaBlocksBackground const isPlaceholder = src === PLACEHOLDER_SRC_PLACEHOLDER && ['image', 'background'].includes(mode) const isEmbed = name === 'core/embed' const hasVideoPreset = attributes?.skaBlocks?.p?.find(({id}: any) => id === 'ska-theme--video') const dispatch = useSkaBlocksDispatch(attributes, setAttributes) const attachment = useSelect(select => select(coreDataStore).getEntityRecord('postType', 'attachment', id), [id]) const setAttribute = (key: keyof BackgroundValue, value: BackgroundValue[typeof key]) => { setAttributes({ skaBlocksBackground: { ...skaBlocksBackground, [key]: value, }, }) } const onSelectMedia = (media: tImage | tVideo) => { dispatch.setAttributes({ skaBlocksBackground: { ...skaBlocksBackground, id: media.id, src: media.type === 'image' && attachment?.media_type === 'image' ? selectImageUrl(media, src, attachment) : media.url, mode: ['image', 'video'].includes(media.type) ? media.type : ( media?.mime_type && media.mime_type.startsWith('video') ? 'video' : 'image' ), }, }) } const onSelectMediaURL = (url: string) => { dispatch.setAttributes({ skaBlocksBackground: { ...skaBlocksBackground, id: undefined, src: url, mode: isVideoUrl(url) ? 'video' : 'image', }, }) } const onClear = () => { dispatch.setAttributes({ skaBlocksBackground: { ...skaBlocksBackground, id: undefined, src: undefined, }, }) } const onSelectPoster = (media: tImage) => { dispatch.setAttributes({ skaBlocksBackground: { ...skaBlocksBackground, posterId: media.id, posterSrc: media.url, }, }) } const onSelectPosterURL = (url: string) => { dispatch.setAttributes({ skaBlocksBackground: { ...skaBlocksBackground, posterId: undefined, posterSrc: url, }, }) } const onClearPoster = () => { dispatch.setAttributes({ skaBlocksBackground: { ...skaBlocksBackground, posterId: undefined, posterSrc: undefined, }, }) } const setMode = (nextMode?: string) => { if(nextMode) { dispatch.setAttributes({ skaBlocksBackground: { ...skaBlocksBackground, mode: nextMode as BackgroundValue['mode'], ...(src && { /** Reset media when switching from or to video as they use different media type. */ ...((mode === 'video' || nextMode === 'video') && { src: undefined, id: undefined, }), }), ...(nextMode === 'placeholder' && { id: undefined, src: PLACEHOLDER_SRC, placeholderIndex: undefined, }), }, }) } } if(isEmbed && !hasVideoPreset) { return null } return <> <> {!src && !isEmbed && ( { dispatch.setAttributes({ skaBlocksBackground: { ...skaBlocksBackground, mode: 'placeholder', src: PLACEHOLDER_SRC, ...(placeholderIndex > 0 && { mode: 'image', src: PLACEHOLDER_SRC_PLACEHOLDER, }), }, }) onClose() }} /> )} {src && ( { onClear() onClose() }} /> )} } /> {mode === 'video' && <> <> {posterSrc && ( { onClearPoster() onClose() }} /> )} } /> } {src && ( {!isEmbed && <> } {src && <> {!isEmbed && <> {(mode === 'placeholder' || isPlaceholder) && <> { setAttributes({ skaBlocksBackground: { ...skaBlocksBackground, placeholderIndex: nextIndex, ...(mode === 'placeholder' && nextIndex > 0 && { mode: 'image', }), ...(nextIndex < 1 && mode !== 'placeholder' && { mode: 'placeholder', }), ...(nextIndex > 0 ? { src: PLACEHOLDER_SRC_PLACEHOLDER, } : { src: PLACEHOLDER_SRC, }), }, }) }} /> } {mode === 'image' && <> value && setAttribute('loading', value)} useDefaultButtons /> {!!id && <> setAttribute('srcset', !srcset)} __nextHasNoMarginBottom /> setAttribute('placeholder', !placeholder)} __nextHasNoMarginBottom /> {placeholder && <> setAttribute('preloadPlaceholder', !preloadPlaceholder)} __nextHasNoMarginBottom /> } } } { if(Number.isNaN(value)) { return } setAttribute('opacity', Number(value) / 100) }} min={1} max={100} __nextHasNoMarginBottom __next40pxDefaultSize /> VALID_BACKGROUND_SIZES.includes(value)) : BACKGROUND_SIZE} value={size} onChange={value => value && setAttribute('size', value)} /> {mode === 'background' && ( value && setAttribute('repeat', value)} /> )} setAttribute('focalPoint', nextFocalPoint)} __nextHasNoMarginBottom /> } {(mode === 'image' || mode === 'background') && id && <> setAttribute('src', src)} /> } {mode === 'video' && <> setAttribute('play', !play)} __nextHasNoMarginBottom /> {posterSrc && <> {posterId && <> setAttribute('posterSrc', src)} /> } setAttribute('preloadPoster', !preloadPoster)} __nextHasNoMarginBottom /> } } } )} } const withBackgroundControls = createHigherOrderComponent( (BlockEdit: any) => (props: tBlockEditProps) => { if(hasBackgroundSupport(props.name)) { return <> } /> } return }, 'withSkaBlocksBackground' ) const getSrc = (skaBlocksBackground: BackgroundValue): string => { if(!skaBlocksBackground) { return '' } const { src, } = skaBlocksBackground if(!src) { return '' } if(src === PLACEHOLDER_SRC_PLACEHOLDER) { const { mode = 'image', } = skaBlocksBackground if(!placeholderImageUrl || !['image', 'background'].includes(mode)) { return '' } const { placeholderIndex = 1, } = skaBlocksBackground // @ts-expect-error const placeholderSrc = sprintf(placeholderImageUrl, placeholderIndex) if(placeholderSrc.indexOf(homeUrl) === 0) { return placeholderSrc.replace(homeUrl, '/') } return placeholderSrc } return src } export const getBackgroundTailwindClassNames = (skaBlocksBackground: BackgroundValue = {}): string => { const src = getSrc(skaBlocksBackground) if(!src) { return '' } return 'has-ska-bg' } const withBackgroundTailwindClassNames = (classNames: string = '', attributes: tBlockAttributes = {}) => { if(getSrc(attributes?.skaBlocksBackground)) { const tailwindClassNames = getBackgroundTailwindClassNames(attributes.skaBlocksBackground) if(classNames.indexOf(tailwindClassNames) === -1) { return cx(tailwindClassNames, classNames) } } return classNames } const getBackgroundElement = (skaBlocksBackground: BackgroundValue, save = false): React.ReactNode => { const { mode = 'image', id, posterSrc, loading = 'lazy', size = BACKGROUND_SIZE[0].value, repeat = BACKGROUND_REPEAT[0].value, opacity = 1, focalPoint = DEFAULT_FOCAL_POINT, play = true, } = skaBlocksBackground const src = getSrc(skaBlocksBackground) if(!src) { return null } const className = cx('ska-bg', [ ...(id ? [`ska-bg-${id}`] : []), ]) if(mode === 'video') { return (