UNPKG

strapi-plugin-icons-field

Version:

A customizable plugin for Strapi to integrate an icon field into your content types.

314 lines (308 loc) 13 kB
import { jsxs, jsx, Fragment } from "react/jsx-runtime"; import React, { useState, useEffect, useMemo } from "react"; import { Field, Modal, Box, Button, Typography, Searchbar } from "@strapi/design-system"; import { useFetchClient } from "@strapi/strapi/admin"; import { P as PLUGIN_ID } from "./index-C9igKtaA.mjs"; import parse from "html-react-parser"; import styled, { useTheme } from "styled-components"; import { CaretDown, Cross } from "@strapi/icons"; import { useIntl } from "react-intl"; function Icon({ icon, ...props }) { const parsedElement = icon && parse(icon.trim()); if (parsedElement && React.isValidElement(parsedElement)) { return React.cloneElement(parsedElement, props); } return null; } const ThemeableIcon = styled.span` display: contents; /* Resolve any currentColor usage to the theme color. */ & > svg { color: ${(p) => p.$color}; } /* * Stroke-based icons carry the stroke on the <svg> root (e.g. fill="none" * stroke="currentColor"). Recolor that stroke, but never force a fill on * the root — doing so would override fill="none" and flood the hollow * shapes with color, turning outline icons into solid silhouettes. */ & > svg[stroke]:not([stroke='none']) { stroke: ${(p) => p.$color}; } /* * Fill-based icons have no stroke on the root. Their paths often carry no * fill attribute and rely on the root fill to render, so force it here. */ & > svg:not([stroke]) { fill: ${(p) => p.$color}; } /* Any explicitly-filled shape gets recolored. */ & > svg *[fill]:not([fill='none']) { fill: ${(p) => p.$color}; } /* * Any explicitly-stroked shape gets its stroke recolored and its fill * cleared, so a forced root fill (see above) can't leak in and fill the * interior of an outline path. */ & > svg *[stroke]:not([stroke='none']) { stroke: ${(p) => p.$color}; fill: none; } `; const hasOwnColors = (svg) => /(?:fill|stroke)\s*=\s*["'](?:#|rgb|hsl|url\(#)/i.test(svg) || /(?:fill|stroke)\s*:\s*(?:#|rgb|hsl)/i.test(svg); const IconPreview = ({ svg, color, style }) => { const icon = /* @__PURE__ */ jsx(Icon, { icon: svg, style }); if (hasOwnColors(svg)) return icon; return /* @__PURE__ */ jsx(ThemeableIcon, { $color: color, children: icon }); }; const IconSelect = (props) => { const theme = useTheme(); const intl = useIntl(); const { label, hint, attribute, disabled, error, name, onChange, placeholder, required, value } = props; const [icons, setIcons] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const { get } = useFetchClient(); const toggleModal = () => setIsModalOpen((prev) => !prev); useEffect(() => { const fetchIcons = async () => { try { let url = `/${PLUGIN_ID}/icons`; if (attribute?.options?.selection) { if (Array.isArray(attribute.options.selection)) { url += `?selection=${encodeURIComponent(attribute.options.selection.join(","))}`; } else { url += `?selection=${encodeURIComponent(attribute.options.selection)}`; } } const { data } = await get(url); setIcons(data || []); } catch (error2) { console.error("Error fetching icons:", error2); } }; fetchIcons(); }, [attribute?.options?.selection]); const outputFormat = attribute?.options?.outputFormat || "svg"; const handleChange = (icon) => { const storedValue = outputFormat === "name" ? icon.name : icon.svg; onChange({ target: { name, value: storedValue, type: attribute.type } }); }; const allIcons = useMemo(() => icons.flatMap((group) => group.icons), [icons]); const selectedIconData = useMemo(() => { if (!value) return void 0; if (outputFormat === "name") { return allIcons.find((icon) => icon.name === value); } return allIcons.find((icon) => icon.svg === value); }, [value, allIcons, outputFormat]); const filteredIcons = useMemo(() => { if (!searchQuery.trim()) return icons; const query = searchQuery.toLowerCase(); return icons.map((group) => ({ ...group, icons: group.icons.filter((icon) => icon.name.toLowerCase().includes(query)) })).filter((group) => group.icons.length > 0); }, [icons, searchQuery]); const handleUnselectedIcon = (e) => { e.stopPropagation(); onChange({ target: { name, value: "", type: attribute.type } }); }; return /* @__PURE__ */ jsxs(Field.Root, { error, hint, required, children: [ /* @__PURE__ */ jsx(Field.Label, { children: label }), /* @__PURE__ */ jsxs( Modal.Root, { labelledBy: "icon-select-modal-title", open: isModalOpen, onOpenChange: setIsModalOpen, children: [ /* @__PURE__ */ jsx(Modal.Trigger, { children: /* @__PURE__ */ jsxs( Box, { display: "flex", style: { alignItems: "center", gap: "12px", position: "relative", width: "100%" }, children: [ /* @__PURE__ */ jsx("div", { style: { position: "relative", flex: 1 }, children: /* @__PURE__ */ jsx( Button, { variant: "tertiary", disabled, style: { gap: "8px", height: "42px", width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between" }, children: /* @__PURE__ */ jsxs( "div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [ selectedIconData ? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( IconPreview, { svg: selectedIconData.svg, color: theme.colors?.neutral800 || "currentColor", style: { height: "24px", display: "block" } } ), /* @__PURE__ */ jsx(Typography, { variant: "pi", children: selectedIconData.name }) ] }) : /* @__PURE__ */ jsx(Typography, { style: { fontWeight: "400" }, children: placeholder ?? intl.formatMessage({ id: "icons-field.select", defaultMessage: "Select an icon" }) }), /* @__PURE__ */ jsx( CaretDown, { color: theme.colors?.neutral500, style: { position: "absolute", right: "16px", height: "16px", width: "16px" } } ) ] } ) } ) }), selectedIconData && /* @__PURE__ */ jsx( "button", { type: "button", onClick: handleUnselectedIcon, style: { position: "absolute", right: "44px", height: "16px", width: "16px", cursor: "pointer", zIndex: 3 }, children: /* @__PURE__ */ jsx(Cross, { color: theme.colors?.neutral500, style: { height: "100%", width: "100%" } }) } ) ] } ) }), /* @__PURE__ */ jsxs(Modal.Content, { children: [ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", textColor: "neutral800", as: "h2", id: "title", children: intl.formatMessage({ id: "icons-field.modal.title", defaultMessage: "Select an icon" }) }) }), /* @__PURE__ */ jsxs(Modal.Body, { children: [ /* @__PURE__ */ jsx(Box, { marginBottom: 4, children: /* @__PURE__ */ jsx( Searchbar, { name: "icon-search", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClear: () => setSearchQuery(""), clearLabel: intl.formatMessage({ id: "icons-field.modal.search.clear", defaultMessage: "Clear search" }), placeholder: intl.formatMessage({ id: "icons-field.modal.search.placeholder", defaultMessage: "Search icons..." }), children: intl.formatMessage({ id: "icons-field.modal.search.placeholder", defaultMessage: "Search icons..." }) } ) }), filteredIcons.length === 0 && /* @__PURE__ */ jsx(Typography, { textColor: "neutral500", children: intl.formatMessage({ id: "icons-field.modal.search.empty", defaultMessage: "No icons found." }) }), filteredIcons.map((group) => /* @__PURE__ */ jsxs(Box, { marginBottom: 6, children: [ /* @__PURE__ */ jsx( Typography, { fontWeight: "bold", textColor: "neutral800", as: "h3", id: "title", marginBottom: 2, children: group.folder } ), /* @__PURE__ */ jsx(Box, { display: "flex", marginTop: 2, style: { flexWrap: "wrap", gap: 6 }, children: group.icons.map((icon) => { const isSelected = outputFormat === "name" ? value === icon.name : value === icon.svg; return /* @__PURE__ */ jsxs( Box, { as: "button", type: "button", "aria-label": icon.name, onClick: () => { handleChange(icon); toggleModal(); }, style: { alignItems: "center", aspectRatio: "1/1", background: isSelected ? theme.colors?.primary100 : theme.colors?.neutral100, border: isSelected ? `2px solid ${theme.colors?.primary600}` : `1px solid ${theme.colors?.neutral200}`, borderRadius: "4px", cursor: "pointer", display: "flex", flexDirection: "column", gap: "2px", height: "100%", justifyContent: "center", padding: "12px", width: "85px" }, children: [ /* @__PURE__ */ jsx( IconPreview, { svg: icon.svg, color: theme.colors?.neutral800 || "currentColor", style: { aspectRatio: "1/1", height: "100%", maxWidth: "25px", width: "100%" } } ), attribute?.options?.showIconLabel && /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", style: { marginTop: 8 }, children: icon.name }) ] }, icon.name ); }) }) ] }, group.folder)) ] }), /* @__PURE__ */ jsx(Modal.Footer, { children: /* @__PURE__ */ jsx(Modal.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: intl.formatMessage({ id: "icons-field.modal.close", defaultMessage: "Close window" }) }) }) }) ] }) ] } ), /* @__PURE__ */ jsx(Field.Hint, {}) ] }); }; export { IconSelect as default };