diff --git a/README.md b/README.md index 5bee44d..14dbf8e 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,11 @@ Upload custom SVG icons and insert them inline in the block editor. **Features:** - Custom SVG icon library with secure sanitization - Inline insertion via RichText toolbar (inherits surrounding font size) -- Standalone SVG Icon block with alignment, size, and color controls +- Standalone metadata-driven SVG Icon block with alignment, native text color, spacing, anchor, and custom class support +- Pixel, `em`, and `rem` sizing with bounded values +- Monochrome or original-color rendering +- Decorative or informative accessibility modes with custom labels +- Searchable, paginated, keyboard-accessible icon picker with recent icons first - Zero frontend footprint when no icons are used **Navigate to:** `?page=functionalities&module=svg-icons` diff --git a/assets/blocks/index.php b/assets/blocks/index.php new file mode 100644 index 0000000..6220032 --- /dev/null +++ b/assets/blocks/index.php @@ -0,0 +1,2 @@ + 0, - rawData: iconsData - }); - - // SVG icon for the toolbar button (flag/bookmark style) var toolbarIcon = el('svg', { xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24', width: 24, - height: 24 + height: 24, + 'aria-hidden': true, + focusable: false }, el('path', { fill: 'currentColor', - d: 'M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2zm0 15l-5-2.18L7 18V5h10v13z' + d: 'M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2zm0 15-5-2.18L7 18V5h10v13z' })); - /** - * Render an SVG string safely as React element - */ - var renderSvgIcon = function (svgString) { - return el('span', { - className: 'func-icon-preview', - dangerouslySetInnerHTML: { __html: svgString } - }); - }; - - /** - * Icon Picker Edit Component - */ - var IconPickerEdit = function (props) { - var value = props.value; - var onChange = props.onChange; - - var stateOpen = useState(false); - var isOpen = stateOpen[0]; - var setIsOpen = stateOpen[1]; - - var stateSearch = useState(''); - var searchTerm = stateSearch[0]; - var setSearchTerm = stateSearch[1]; + function getRecentSlugs() { + try { + var value = JSON.parse(window.localStorage.getItem('functionalitiesSvgRecent') || '[]'); + return Array.isArray(value) ? value : []; + } catch (error) { + return []; + } + } - // Filter icons - var filteredIcons = allIcons; - if (searchTerm) { - var term = searchTerm.toLowerCase(); - filteredIcons = allIcons.filter(function (icon) { - return (icon.name && icon.name.toLowerCase().indexOf(term) !== -1) || - (icon.slug && icon.slug.toLowerCase().indexOf(term) !== -1); + function rememberIcon(slug) { + try { + var recent = getRecentSlugs().filter(function (item) { + return item !== slug; }); + recent.unshift(slug); + window.localStorage.setItem('functionalitiesSvgRecent', JSON.stringify(recent.slice(0, 8))); + } catch (error) { + // Storage may be disabled. Icon selection must still work. } + } - // Handle icon insertion - use shortcode for stability - var onInsertIcon = useCallback(function (icon) { - log('Inserting icon', icon.slug); - - // Insert shortcode directly (more stable than HTML tags) - var shortcode = '[func_icon name="' + icon.slug + '"]'; - - // Create rich text value from text and insert at current position - var iconValue = create({ text: shortcode }); - onChange(insert(value, iconValue)); + function requestIcons(search, page, include) { + var key = [search || '', page || 1, include || ''].join('|'); + if (requestCache[key]) { + return Promise.resolve(requestCache[key]); + } - setIsOpen(false); - setSearchTerm(''); - }, [value, onChange]); + var query = '?per_page=' + perPage + '&page=' + (page || 1); + if (search) { + query += '&search=' + encodeURIComponent(search); + } + if (include) { + query += '&include=' + encodeURIComponent(include); + } - // Handle toggle - var onToggle = useCallback(function () { - log('Toggling icon picker', { wasOpen: isOpen, willBeOpen: !isOpen }); - setIsOpen(!isOpen); - if (isOpen) { - setSearchTerm(''); - } - }, [isOpen]); + return apiFetch({ path: restPath + query }).then(function (icons) { + requestCache[key] = Array.isArray(icons) ? icons : []; + return requestCache[key]; + }); + } - // Handle close - var onClose = useCallback(function () { - log('Closing icon picker'); - setIsOpen(false); - setSearchTerm(''); - }, []); + function renderSvgIcon(svgString) { + return el('span', { + className: 'func-icon-preview', + 'aria-hidden': true, + dangerouslySetInnerHTML: { __html: svgString } + }); + } - // Build icon buttons - var iconButtons = []; - if (filteredIcons.length === 0) { - var noIconsText = allIcons.length === 0 - ? (i18n.noIcons || __('No icons available. Add icons in Functionalities > SVG Icons.', 'functionalities')) - : __('No matching icons found.', 'functionalities'); - iconButtons.push( - el('p', { - key: 'empty', - className: 'func-icon-empty' - }, noIconsText) + function IconPicker(props) { + var searchState = useState(''); + var search = searchState[0]; + var setSearch = searchState[1]; + var iconsState = useState([]); + var icons = iconsState[0]; + var setIcons = iconsState[1]; + var pageState = useState(1); + var page = pageState[0]; + var setPage = pageState[1]; + var moreState = useState(false); + var hasMore = moreState[0]; + var setHasMore = moreState[1]; + var loadingState = useState(false); + var loading = loadingState[0]; + var setLoading = loadingState[1]; + var errorState = useState(false); + var hasError = errorState[0]; + var setHasError = errorState[1]; + + useEffect(function () { + var active = true; + var timer = window.setTimeout(function () { + setLoading(true); + setHasError(false); + requestIcons(search, 1, '').then(function (results) { + if (!active) { + return; + } + var recent = getRecentSlugs(); + results.sort(function (first, second) { + var firstIndex = recent.indexOf(first.slug); + var secondIndex = recent.indexOf(second.slug); + firstIndex = firstIndex === -1 ? 999 : firstIndex; + secondIndex = secondIndex === -1 ? 999 : secondIndex; + return firstIndex - secondIndex; + }); + setIcons(results); + setPage(1); + setHasMore(results.length === perPage); + setLoading(false); + }).catch(function () { + if (active) { + setIcons([]); + setHasError(true); + setLoading(false); + } + }); + }, 200); + + return function () { + active = false; + window.clearTimeout(timer); + }; + }, [search]); + + var loadMore = useCallback(function () { + var nextPage = page + 1; + setLoading(true); + requestIcons(search, nextPage, '').then(function (results) { + setIcons(icons.concat(results)); + setPage(nextPage); + setHasMore(results.length === perPage); + setLoading(false); + }).catch(function () { + setHasError(true); + setLoading(false); + }); + }, [icons, page, search]); + + var chooseIcon = function (icon) { + rememberIcon(icon.slug); + props.onSelect(icon); + props.onClose(); + }; + + var content; + if (hasError) { + content = el(Notice, { + status: 'error', + isDismissible: false + }, i18n.loadError || __('Icons could not be loaded. Try again.', 'functionalities')); + } else if (!loading && icons.length === 0) { + content = el('p', { className: 'func-icon-empty' }, + search + ? (i18n.noMatchingIcons || __('No matching icons found.', 'functionalities')) + : (i18n.noIcons || __('No icons found. Add icons in Functionalities > SVG Icons.', 'functionalities')) ); } else { - filteredIcons.forEach(function (icon) { - iconButtons.push( - el('button', { + content = el(Fragment, {}, + el('div', { + className: 'func-icon-grid', + role: 'list', + 'aria-label': i18n.selectIcon || __('Select icon', 'functionalities') + }, icons.map(function (icon) { + var selected = props.selectedSlug === icon.slug; + return el('button', { key: icon.slug, type: 'button', - className: 'func-icon-btn', - onClick: function () { onInsertIcon(icon); }, - title: icon.name - }, renderSvgIcon(icon.svg)) - ); - }); - } - - // Build popover - var popover = null; - if (isOpen) { - popover = el(Popover, { - position: 'bottom center', - onClose: onClose, - className: 'func-svg-icon-popover', - focusOnMount: 'firstElement' - }, - el('div', { className: 'func-svg-icon-picker' }, - el('div', { className: 'func-icon-search-wrapper' }, - el('input', { - type: 'search', - value: searchTerm, - onChange: function (e) { setSearchTerm(e.target.value); }, - placeholder: i18n.searchIcons || __('Search icons...', 'functionalities'), - className: 'func-icon-search-input' - }) - ), - el('div', { className: 'func-icon-grid' }, iconButtons) - ) + role: 'listitem', + className: 'func-icon-btn' + (selected ? ' is-selected' : ''), + onClick: function () { chooseIcon(icon); }, + title: icon.name || icon.slug, + 'aria-label': icon.name || icon.slug, + 'aria-pressed': selected + }, renderSvgIcon(icon.svg)); + })), + loading && el('div', { + className: 'func-icon-loading', + 'aria-label': i18n.loadingIcons || __('Loading icons…', 'functionalities') + }, el(Spinner)), + hasMore && !loading && el(Button, { + variant: 'secondary', + onClick: loadMore, + className: 'func-icon-load-more' + }, i18n.loadMore || __('Load more', 'functionalities')) ); } - // Use RichTextToolbarButton for main toolbar placement + return el(Popover, { + position: 'bottom center', + onClose: props.onClose, + className: 'func-svg-icon-popover', + focusOnMount: 'firstElement' + }, el('div', { className: 'func-svg-icon-picker' }, + el(SearchControl, { + label: i18n.searchIcons || __('Search icons', 'functionalities'), + value: search, + onChange: setSearch, + className: 'func-icon-search' + }), + content + )); + } + + function InlineIconPicker(props) { + var openState = useState(false); + var isOpen = openState[0]; + var setIsOpen = openState[1]; + + var insertIcon = useCallback(function (icon) { + var shortcode = '[func_icon name="' + icon.slug + '"]'; + var iconValue = wp.richText.create({ text: shortcode }); + props.onChange(wp.richText.insert(props.value, iconValue)); + }, [props.value, props.onChange]); + return el(Fragment, {}, el(RichTextToolbarButton, { icon: toolbarIcon, - title: i18n.insertIcon || __('Insert Icon', 'functionalities'), - onClick: onToggle, + title: i18n.insertIcon || __('Insert icon shortcode', 'functionalities'), + onClick: function () { setIsOpen(!isOpen); }, isActive: isOpen }), - popover + isOpen && el(IconPicker, { + onSelect: insertIcon, + onClose: function () { setIsOpen(false); }, + selectedSlug: '' + }) ); - }; + } - // Register format type on DOM ready - wp.domReady(function () { - log('DOM ready - registering format type'); + function SvgIconEdit(props) { + var attributes = props.attributes; + var setAttributes = props.setAttributes; + var openState = useState(false); + var isOpen = openState[0]; + var setIsOpen = openState[1]; + var selectedState = useState(null); + var selectedIcon = selectedState[0]; + var setSelectedIcon = selectedState[1]; + var missingState = useState(false); + var isMissing = missingState[0]; + var setIsMissing = missingState[1]; + var unit = ['px', 'em', 'rem'].indexOf(attributes.sizeUnit) !== -1 ? attributes.sizeUnit : 'px'; + var size = typeof attributes.size === 'number' ? attributes.size : 48; + var mode = attributes.colorMode === 'original' ? 'original' : 'monochrome'; + + useEffect(function () { + var active = true; + if (!attributes.iconSlug) { + setSelectedIcon(null); + setIsMissing(false); + return function () { active = false; }; + } + requestIcons('', 1, attributes.iconSlug).then(function (results) { + if (active) { + setSelectedIcon(results[0] || null); + setIsMissing(results.length === 0); + } + }).catch(function () { + if (active) { + setSelectedIcon(null); + setIsMissing(true); + } + }); + return function () { active = false; }; + }, [attributes.iconSlug]); + + var selectIcon = function (icon) { + setAttributes({ iconSlug: icon.slug }); + setSelectedIcon(icon); + setIsMissing(false); + }; + var maxSize = unit === 'px' ? 512 : 32; + var minSize = unit === 'px' ? 8 : 0.5; + var step = unit === 'px' ? 1 : 0.1; + var nativeColor = attributes.style && attributes.style.color ? attributes.style.color.text : ''; + var previewStyle = { + '--func-icon-size': size + unit, + textAlign: ['left', 'center', 'right'].indexOf(attributes.align) !== -1 ? attributes.align : undefined, + color: nativeColor || attributes.color || undefined + }; + var blockProps = useBlockProps({ + className: 'func-svg-icon-block-wrapper is-color-' + mode, + style: previewStyle + }); - try { - registerFormatType('functionalities/svg-icon', { - title: i18n.insertIcon || __('Insert Icon', 'functionalities'), + return el(Fragment, {}, + el(BlockControls, {}, + el(AlignmentToolbar, { + value: attributes.align === 'none' ? undefined : attributes.align, + onChange: function (align) { setAttributes({ align: align || 'none' }); } + }), + el(ToolbarGroup, {}, + el(ToolbarButton, { + icon: toolbarIcon, + title: selectedIcon + ? (i18n.changeIcon || __('Change icon', 'functionalities')) + : (i18n.selectIcon || __('Select icon', 'functionalities')), + onClick: function () { setIsOpen(true); }, + isPressed: isOpen + }) + ) + ), + el(InspectorControls, {}, + el(PanelBody, { + title: i18n.iconSettings || __('Icon settings', 'functionalities'), + initialOpen: true + }, + el(RangeControl, { + label: i18n.iconSize || __('Icon size', 'functionalities'), + value: size, + onChange: function (value) { setAttributes({ size: value }); }, + min: minSize, + max: maxSize, + step: step + }), + el(SelectControl, { + label: i18n.sizeUnit || __('Size unit', 'functionalities'), + value: unit, + options: [ + { label: 'px', value: 'px' }, + { label: 'em', value: 'em' }, + { label: 'rem', value: 'rem' } + ], + onChange: function (value) { + var nextSize = value === 'px' ? Math.max(8, Math.min(512, size)) : Math.max(0.5, Math.min(32, size)); + setAttributes({ sizeUnit: value, size: nextSize }); + } + }), + el(SelectControl, { + label: i18n.colorMode || __('Color mode', 'functionalities'), + value: mode, + options: [ + { label: i18n.monochrome || __('Monochrome (inherit text color)', 'functionalities'), value: 'monochrome' }, + { label: i18n.originalColors || __('Original SVG colors', 'functionalities'), value: 'original' } + ], + onChange: function (value) { setAttributes({ colorMode: value }); } + }), + el(ToggleControl, { + label: i18n.decorative || __('Decorative icon', 'functionalities'), + help: i18n.decorativeHelp || __('Decorative icons are hidden from assistive technology.', 'functionalities'), + checked: attributes.decorative !== false, + onChange: function (value) { setAttributes({ decorative: value }); } + }), + attributes.decorative === false && el(TextControl, { + label: i18n.accessibility || __('Accessibility label', 'functionalities'), + value: attributes.label || '', + onChange: function (value) { setAttributes({ label: value }); } + }) + ) + ), + el('div', blockProps, + isMissing && el(Notice, { + status: 'warning', + isDismissible: false + }, i18n.missingIcon || __('The selected icon is no longer in the library. Choose a replacement.', 'functionalities')), + selectedIcon ? el('span', { + className: 'func-svg-icon-block-render', + 'aria-label': attributes.decorative === false ? (attributes.label || selectedIcon.name) : undefined, + 'aria-hidden': attributes.decorative === false ? undefined : true, + role: attributes.decorative === false ? 'img' : undefined, + dangerouslySetInnerHTML: { __html: selectedIcon.svg } + }) : el(Button, { + variant: 'primary', + onClick: function () { setIsOpen(true); }, + className: 'func-svg-icon-block-placeholder' + }, i18n.selectIcon || __('Select icon', 'functionalities')), + isOpen && el(IconPicker, { + onSelect: selectIcon, + onClose: function () { setIsOpen(false); }, + selectedSlug: attributes.iconSlug || '' + }) + ) + ); + } + + wp.domReady(function () { + if (wp.richText && RichTextToolbarButton && wp.richText.registerFormatType) { + wp.richText.registerFormatType('functionalities/svg-icon', { + title: i18n.insertIcon || __('Insert icon shortcode', 'functionalities'), tagName: 'i', className: 'func-icon', attributes: { dataIcon: 'data-icon' }, - edit: IconPickerEdit, + edit: InlineIconPicker, object: true }); - log('Format type registered successfully: functionalities/svg-icon'); - } catch (error) { - log('ERROR registering format type', error); } - // Register Block Type - try { - registerBlockType('functionalities/svg-icon-block', { - title: i18n.blockTitle || __('SVG Icon', 'functionalities'), - description: i18n.blockDesc || __('Insert an SVG icon from your library as a block.', 'functionalities'), - icon: toolbarIcon, - category: 'design', - attributes: { - iconSlug: { type: 'string' }, - size: { type: 'number', default: 48 }, - align: { type: 'string', default: 'none' }, - color: { type: 'string' } - }, - edit: function (props) { - var attributes = props.attributes; - var setAttributes = props.setAttributes; - var iconSlug = attributes.iconSlug; - var size = attributes.size; - var align = attributes.align; - var color = attributes.color; - - var stateOpen = useState(false); - var isOpen = stateOpen[0]; - var setIsOpen = stateOpen[1]; - - var stateSearch = useState(''); - var searchTerm = stateSearch[0]; - var setSearchTerm = stateSearch[1]; - - var selectedIcon = allIcons.find(function (i) { return i.slug === iconSlug; }); - - var filteredIcons = allIcons; - if (searchTerm) { - var term = searchTerm.toLowerCase(); - filteredIcons = allIcons.filter(function (icon) { - return (icon.name && icon.name.toLowerCase().indexOf(term) !== -1) || - (icon.slug && icon.slug.toLowerCase().indexOf(term) !== -1); - }); - } - - var onSelectIcon = function (icon) { - setAttributes({ iconSlug: icon.slug }); - setIsOpen(false); - }; - - var iconButtons = []; - if (filteredIcons.length === 0) { - iconButtons.push(el('p', { key: 'empty', className: 'func-icon-empty' }, i18n.noIcons || __('No matching icons found.', 'functionalities'))); - } else { - filteredIcons.forEach(function (icon) { - iconButtons.push( - el('button', { - key: icon.slug, - type: 'button', - className: 'func-icon-btn' + (iconSlug === icon.slug ? ' is-selected' : ''), - onClick: function () { onSelectIcon(icon); }, - title: icon.name - }, renderSvgIcon(icon.svg)) - ); - }); - } - - return el(Fragment, {}, - el(BlockControls, {}, - el(AlignmentToolbar, { - value: align, - onChange: function (newAlign) { setAttributes({ align: newAlign }); } - }), - el(ToolbarGroup, {}, - el(ToolbarButton, { - icon: toolbarIcon, - title: i18n.changeIcon || __('Change Icon', 'functionalities'), - onClick: function () { setIsOpen(true); } - }) - ) - ), - el(InspectorControls, {}, - el(PanelBody, { title: i18n.iconSettings || __('Icon Settings', 'functionalities') }, - el(RangeControl, { - label: i18n.iconSize || __('Icon Size (px)', 'functionalities'), - value: size, - onChange: function (newSize) { setAttributes({ size: newSize }); }, - min: 10, - max: 300 - }), - el('p', {}, i18n.iconColor || __('Icon Color', 'functionalities')), - el(ColorPalette, { - value: color, - onChange: function (newColor) { setAttributes({ color: newColor }); } - }) - ) - ), - el('div', { - className: 'func-svg-icon-block-wrapper align' + align, - style: { - textAlign: align === 'left' || align === 'right' || align === 'center' ? align : undefined - } - }, - selectedIcon ? el('div', { - className: 'func-svg-icon-block-render', - style: { - width: size + 'px', - height: size + 'px', - color: color, - display: 'inline-block' - }, - dangerouslySetInnerHTML: { __html: selectedIcon.svg } - }) : el(Button, { - isPrimary: true, - onClick: function () { setIsOpen(true); }, - className: 'func-svg-icon-block-placeholder' - }, i18n.selectIcon || __('Select Icon', 'functionalities')), - isOpen && el(Popover, { - onClose: function () { setIsOpen(false); }, - className: 'func-svg-icon-popover' - }, - el('div', { className: 'func-svg-icon-picker' }, - el('div', { className: 'func-icon-search-wrapper' }, - el('input', { - type: 'search', - value: searchTerm, - onChange: function (e) { setSearchTerm(e.target.value); }, - placeholder: i18n.searchIcons || __('Search icons...', 'functionalities'), - className: 'func-icon-search-input' - }) - ), - el('div', { className: 'func-icon-grid' }, iconButtons) - ) - ) - ) - ); - }, - save: function () { - return null; - } - }); - log('Block type registered successfully: functionalities/svg-icon-block'); - } catch (error) { - log('ERROR registering block type', error); - } + var metadata = config.blockMetadata || {}; + var blockName = metadata.name || 'functionalities/svg-icon-block'; + var settings = Object.assign({}, metadata, { + icon: toolbarIcon, + edit: SvgIconEdit, + save: function () { return null; } + }); + delete settings.name; + delete settings.$schema; + registerBlockType(blockName, settings); }); - })(window.wp); diff --git a/includes/features/class-svg-icons.php b/includes/features/class-svg-icons.php index a96a1bf..79c31b6 100644 --- a/includes/features/class-svg-icons.php +++ b/includes/features/class-svg-icons.php @@ -69,14 +69,14 @@ class SVG_Icons { 'polyline', 'polygon', 'defs', - 'clipPath', + 'clippath', 'mask', 'use', 'symbol', 'title', 'desc', - 'linearGradient', - 'radialGradient', + 'lineargradient', + 'radialgradient', 'stop', ); @@ -137,6 +137,36 @@ class SVG_Icons { 'enable-background', ); + /** + * CSS properties allowed in an imported SVG style attribute. + * + * @var array + */ + private static $allowed_style_properties = array( + 'color', + 'fill', + 'fill-opacity', + 'fill-rule', + 'stroke', + 'stroke-width', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-opacity', + 'clip-rule', + 'opacity', + 'stop-color', + 'stop-opacity', + ); + + /** + * Request-local counter used to prevent duplicate SVG definition IDs. + * + * @var int + */ + private static $render_instance = 0; + /** * Initialize the SVG icons module. * @@ -162,6 +192,7 @@ public static function init(): void { // Register block editor assets. \add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_editor_assets' ) ); \add_action( 'enqueue_block_assets', array( __CLASS__, 'enqueue_editor_styles' ) ); + \add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) ); // Register AJAX handlers. \add_action( 'wp_ajax_functionalities_svg_icon_save', array( __CLASS__, 'ajax_save_icon' ) ); @@ -190,21 +221,14 @@ public static function register_block(): void { return; } + $metadata = FUNCTIONALITIES_DIR . 'assets/blocks/svg-icon/block.json'; + if ( ! file_exists( $metadata ) ) { + return; + } + \register_block_type( - 'functionalities/svg-icon-block', + $metadata, array( - 'attributes' => array( - 'iconSlug' => array( 'type' => 'string' ), - 'size' => array( - 'type' => 'number', - 'default' => 48, - ), - 'align' => array( - 'type' => 'string', - 'default' => 'none', - ), - 'color' => array( 'type' => 'string' ), - ), 'render_callback' => array( __CLASS__, 'render_block' ), ) ); @@ -223,43 +247,57 @@ public static function render_block( array $attributes ): string { return ''; } - $size = isset( $attributes['size'] ) ? intval( $attributes['size'] ) : 48; - $align = isset( $attributes['align'] ) ? $attributes['align'] : 'none'; - $color = isset( $attributes['color'] ) ? $attributes['color'] : ''; - - $svg = self::render_icon( $slug, 'func-svg-icon-block' ); + $unit = isset( $attributes['sizeUnit'] ) && in_array( $attributes['sizeUnit'], array( 'px', 'em', 'rem' ), true ) + ? $attributes['sizeUnit'] + : 'px'; + $size = isset( $attributes['size'] ) && is_numeric( $attributes['size'] ) + ? (float) $attributes['size'] + : 48; + $size = 'px' === $unit + ? max( 8, min( 512, $size ) ) + : max( 0.5, min( 32, $size ) ); + $size = rtrim( rtrim( number_format( $size, 2, '.', '' ), '0' ), '.' ); + + $align = isset( $attributes['align'] ) && in_array( $attributes['align'], array( 'left', 'center', 'right' ), true ) + ? $attributes['align'] + : 'none'; + $color = isset( $attributes['color'] ) ? \sanitize_hex_color( $attributes['color'] ) : ''; + $mode = isset( $attributes['colorMode'] ) && 'original' === $attributes['colorMode'] + ? 'original' + : 'monochrome'; + $decorative = ! isset( $attributes['decorative'] ) || (bool) $attributes['decorative']; + $label = isset( $attributes['label'] ) ? \sanitize_text_field( $attributes['label'] ) : ''; + + $svg = self::render_icon( + $slug, + 'func-svg-icon-block', + array( + 'block' => true, + 'color_mode' => $mode, + 'decorative' => $decorative, + 'label' => $label, + ) + ); if ( empty( $svg ) ) { return ''; } - // Apply custom size and color. - $styles = array( - 'width' => $size . 'px', - 'height' => $size . 'px', - 'display' => 'inline-block', - 'vertical-align' => 'middle', - 'fill' => 'currentColor', - ); + $wrapper_styles = '--func-icon-size:' . $size . $unit . ';line-height:0;'; + $wrapper_styles .= 'none' !== $align ? 'text-align:' . $align . ';' : ''; + $wrapper_styles .= $color ? 'color:' . $color . ';' : ''; - if ( ! empty( $color ) ) { - $styles['color'] = $color; - } - - $style_attr = ''; - foreach ( $styles as $prop => $val ) { - $style_attr .= $prop . ':' . $val . ';'; - } - - // Replace the style attribute added by render_icon. - $svg = preg_replace( '/style="[^"]*"/', 'style="' . \esc_attr( $style_attr ) . '"', $svg, 1 ); - - $wrapper_styles = 'margin: 1em 0;'; - if ( in_array( $align, array( 'left', 'right', 'center' ), true ) ) { - $wrapper_styles .= 'text-align:' . $align . ';'; + $wrapper_attributes = array( + 'class' => 'func-svg-icon-block-wrapper is-color-' . $mode, + 'style' => $wrapper_styles, + ); + if ( function_exists( 'get_block_wrapper_attributes' ) ) { + $wrapper = \get_block_wrapper_attributes( $wrapper_attributes ); + } else { + $wrapper = 'class="' . \esc_attr( $wrapper_attributes['class'] ) . '" style="' . \esc_attr( $wrapper_attributes['style'] ) . '"'; } - return '
'; + return '