@@ -20,6 +20,7 @@ import { addStyles } from './style'
2020 * WordPress dependencies
2121 */
2222import { useBlockEditContext } from '@wordpress/block-editor'
23+ import { dispatch , select } from '@wordpress/data'
2324import {
2425 useMemo , useState , useRef , useEffect , renderToString ,
2526} from '@wordpress/element'
@@ -57,6 +58,67 @@ const LinearGradient = ( {
5758
5859const NOOP = ( ) => { }
5960
61+ const getSvgDef = ( href , viewBox = '0 0 24 24' ) => {
62+ return `<svg viewBox="${ viewBox } "><use href="${ href } " xlink:href="${ href } "></use></svg>`
63+ }
64+
65+ const generateIconId = ( ) => {
66+ return Math . floor ( Math . random ( ) * new Date ( ) . getTime ( ) ) % 100000
67+ }
68+
69+ /**
70+ * Extract viewBox, width, and height from SVG string without DOM manipulation
71+ * Only checks for the specific attributes we need (case-insensitive)
72+ *
73+ * @param {string } svgString The SVG string to parse
74+ * @return {Object } Object with viewBox, width, and height
75+ */
76+ const extractSVGDimensions = svgString => {
77+ if ( ! svgString || typeof svgString !== 'string' ) {
78+ return {
79+ viewBox : null ,
80+ width : null ,
81+ height : null ,
82+ }
83+ }
84+
85+ // Find the opening <svg> tag
86+ const svgTagMatch = svgString . match ( / < s v g \s * [ ^ > ] * > / i )
87+ if ( ! svgTagMatch ) {
88+ return {
89+ viewBox : null ,
90+ width : null ,
91+ height : null ,
92+ }
93+ }
94+
95+ const svgTag = svgTagMatch [ 0 ]
96+
97+ // Extract only the attributes we need (case-insensitive)
98+ // Pattern: attribute name (case-insensitive) = "value" or 'value' or value
99+ const getAttribute = attrName => {
100+ const regex = new RegExp ( `${ attrName } \\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))` , 'i' )
101+ const match = svgTag . match ( regex )
102+ if ( match ) {
103+ return match [ 1 ] || match [ 2 ] || match [ 3 ] || ''
104+ }
105+ return null
106+ }
107+
108+ const viewBox = getAttribute ( 'viewBox' )
109+ const widthStr = getAttribute ( 'width' )
110+ const heightStr = getAttribute ( 'height' )
111+
112+ const width = widthStr ? parseInt ( widthStr , 10 ) : null
113+ const height = heightStr ? parseInt ( heightStr , 10 ) : null
114+
115+ return {
116+ viewBox,
117+ width,
118+ height,
119+ }
120+ }
121+
60122export const Icon = props => {
61123 const {
62124 attrNameTemplate = '%s' ,
@@ -122,7 +184,114 @@ export const Icon = props => {
122184
123185 const ShapeComp = useMemo ( ( ) => getShapeSVG ( getAttribute ( 'backgroundShape' ) || 'blob1' ) , [ getAttribute ( 'backgroundShape' ) ] )
124186
125- const icon = value || getAttribute ( 'icon' )
187+ const _icon = value || getAttribute ( 'icon' )
188+ const currentIconRef = useRef ( _icon )
189+ const processedIconRef = useRef ( null )
190+ const lastIconValueRef = useRef ( null )
191+ const [ icon , setIcon ] = useState ( _icon )
192+
193+ const addPageIconCount = ( svg , id ) => {
194+ dispatch ( 'stackable/page-icons' ) . addPageIcon ( svg , id )
195+ }
196+
197+ useEffect ( ( ) => {
198+ currentIconRef . current = _icon
199+
200+ // Skip if we've already processed this icon
201+ if ( processedIconRef . current === _icon ) {
202+ return
203+ }
204+
205+ // Check if icon exists in pageIcons Map
206+ // The Map structure is: [SVG string (key), { id: iconId, count: number } (value)]
207+ if ( _icon ) {
208+ const iconStr = String ( _icon )
209+ let originalSvg = null
210+ let iconId = null
211+
212+ // Get the current state of the store
213+ const pageIcons = select ( 'stackable/page-icons' ) . getPageIcons ( )
214+
215+ // First, check if icon already exists in the store
216+ if ( pageIcons . has ( iconStr ) ) {
217+ // Icon exists, use the existing ID and increment count
218+ const iconData = pageIcons . get ( iconStr )
219+ iconId = iconData ?. id || iconData
220+ originalSvg = iconStr
221+ addPageIconCount ( iconStr , iconId )
222+
223+ // Re-check after dispatch to get the actual ID (handles race conditions)
224+ const updatedPageIcons = select ( 'stackable/page-icons' ) . getPageIcons ( )
225+ if ( updatedPageIcons . has ( iconStr ) ) {
226+ const iconData = updatedPageIcons . get ( iconStr )
227+ iconId = iconData ?. id || iconData || iconId
228+ }
229+ } else if ( iconStr && iconStr . trim ( ) . startsWith ( '<svg' ) && ! iconStr . includes ( '<use' ) ) {
230+ // Icon doesn't exist, generate new ID and add it
231+ originalSvg = iconStr
232+ iconId = generateIconId ( )
233+ addPageIconCount ( iconStr , iconId )
234+
235+ // After dispatch, immediately check the store again to get the actual ID
236+ // This handles the race condition where another component might have added
237+ // the same icon with a different ID
238+ const updatedPageIcons = select ( 'stackable/page-icons' ) . getPageIcons ( )
239+ if ( updatedPageIcons . has ( iconStr ) ) {
240+ const iconData = updatedPageIcons . get ( iconStr )
241+ // Use the ID from the store
242+ iconId = iconData ?. id || iconData || iconId
243+ }
244+ }
245+
246+ if ( originalSvg && iconId ) {
247+ let viewBox = '0 0 24 24' // Default viewBox
248+ // Extract viewBox from the original SVG for proper dimensions
249+ const {
250+ viewBox : vb ,
251+ width,
252+ height,
253+ } = extractSVGDimensions ( originalSvg )
254+ if ( vb ) {
255+ viewBox = vb
256+ } else {
257+ // Fallback to width/height if viewBox is not available
258+ const finalWidth = width || 24
259+ const finalHeight = height || 24
260+ viewBox = `0 0 ${ finalWidth } ${ finalHeight } `
261+ }
262+ const newIcon = getSvgDef ( `#stk-page-icons__${ iconId } ` , viewBox )
263+
264+ // Only update state if the icon actually changed
265+ if ( newIcon !== lastIconValueRef . current ) {
266+ setIcon ( newIcon )
267+ lastIconValueRef . current = newIcon
268+ }
269+ processedIconRef . current = _icon
270+ } else if ( ! _icon ) {
271+ // Clear processed ref when icon is removed
272+ processedIconRef . current = null
273+ if ( lastIconValueRef . current !== null ) {
274+ setIcon ( null )
275+ lastIconValueRef . current = null
276+ }
277+ }
278+ } else {
279+ processedIconRef . current = null
280+ if ( lastIconValueRef . current !== null ) {
281+ setIcon ( null )
282+ lastIconValueRef . current = null
283+ }
284+ }
285+ } , [ _icon ] )
286+
287+ useEffect ( ( ) => {
288+ return ( ) => {
289+ if ( currentIconRef . current ) {
290+ dispatch ( 'stackable/page-icons' ) . removePageIcon ( currentIconRef . current )
291+ }
292+ }
293+ } , [ ] )
294+
126295 if ( ! icon ) {
127296 return null
128297 }
@@ -171,6 +340,7 @@ export const Icon = props => {
171340 __deprecateUseRef = { popoverEl }
172341 onClose = { ( ) => setIsOpen ( false ) }
173342 onChange = { icon => {
343+ dispatch ( 'stackable/page-icons' ) . removePageIcon ( _icon )
174344 if ( onChange === NOOP ) {
175345 updateAttributeHandler ( 'icon' ) ( icon )
176346 } else {
0 commit comments