1+ /**
2+ * Cloudinary Preview Update Handler
3+ * Manages image preview updates with Cloudinary transformed images in Page Builder
4+ */
15define (
2- [ 'jquery' , 'Magento_PageBuilder/js/events' , 'mage/url' ] ,
3- function ( $ , _PBEvents , urlBuilder ) {
4- 'use strict'
6+ [ 'jquery' , 'Magento_PageBuilder/js/events' ] ,
7+ function ( $ , _PBEvents ) {
8+ 'use strict' ;
9+
10+ /**
11+ * @param {Object } config - Configuration object
12+ * @param {string } config.ajaxUrl - URL for AJAX requests
13+ * @param {string } config.saveButtonSelector - Selector for save button (default: '#save-button')
14+ * @param {Element } element - DOM element
15+ * @returns {Object } updateHandler instance
16+ */
517 return function ( config , element ) {
6- var updateHandler = {
7- images : [ ] ,
18+ let updateHandler = {
19+ images : { } ,
20+ pendingRequests : { } ,
21+ processedImages : new Set ( ) ,
22+ saveButtonSelector : config . saveButtonSelector || '#save-button' ,
23+ eventHandlers : [ ] ,
24+
25+ /**
26+ * Initialize the handler and set up event listeners
27+ */
828 init : function ( ) {
929 let self = this ;
10- _PBEvents . on ( 'image:renderAfter' , function ( event ) {
11- let elem = event . element , key = event . id ;
12- let image = $ ( elem ) . find ( 'img' ) ;
13- let src = { 'remote_image' : image . attr ( 'src' ) } ;
14- self . images . push ( src ) ;
30+
31+ // Store event handler references for cleanup
32+ let renderAfterHandler = function ( event ) {
33+ let elem = event . element ,
34+ key = event . id ,
35+ image = $ ( elem ) . find ( 'img' ) ,
36+ imageSrc = image . attr ( 'src' ) ;
37+
38+ // Skip if no image source or already processed
39+ if ( ! imageSrc || self . processedImages . has ( imageSrc ) ) {
40+ return ;
41+ }
42+
43+ self . images [ key ] = {
44+ key : key ,
45+ remote_image : imageSrc
46+ } ;
1547
1648 self . update ( key ) ;
49+ } ;
50+
51+ let saveButtonHandler = function ( ) {
52+ self . restoreOriginalImages ( ) ;
53+ } ;
54+
55+ // Attach event handlers
56+ _PBEvents . on ( 'image:renderAfter' , renderAfterHandler ) ;
57+ $ ( self . saveButtonSelector ) . on ( 'click' , saveButtonHandler ) ;
58+
59+ // Store handlers for cleanup
60+ self . eventHandlers . push ( {
61+ event : 'image:renderAfter' ,
62+ handler : renderAfterHandler
1763 } ) ;
18- $ ( '#save-button' ) . on ( 'click' , function ( e ) {
19- self . images . forEach ( function ( elem ) {
20- if ( elem . cld_image ) {
21- let cld_src = elem . cld_image ;
22- let img = $ ( 'img[src="' + cld_src + '"]' ) ;
23- if ( img . length ) {
24- img . attr ( 'src' , elem . remote_image ) ;
25- }
26- }
27- } ) ;
64+ self . eventHandlers . push ( {
65+ selector : self . saveButtonSelector ,
66+ event : 'click' ,
67+ handler : saveButtonHandler
2868 } ) ;
69+
70+ return self ;
2971 } ,
30- update : function ( key ) {
72+
73+ /**
74+ * Restore original images before save
75+ */
76+ restoreOriginalImages : function ( ) {
3177 let self = this ;
32- this . images . forEach ( function ( elem , ind ) {
33- $ . ajax ( {
34- url : config . ajaxUrl ,
35- type : 'POST' ,
36- dataType : 'json' ,
37- data : elem ,
38- success : function ( image ) {
39- self . images [ ind ] . cld_image = image ;
40- let img = $ ( 'img[src="' + self . images [ ind ] . remote_image + '"]' ) ;
41- if ( img . length ) {
42- img . attr ( 'src' , self . images [ ind ] . cld_image ) ;
43- }
44- } ,
45- error : function ( xhr , textStatus , errorThrown ) {
46- console . log ( 'Error:' , textStatus , errorThrown ) ;
78+
79+ $ . each ( self . images , function ( _ , elem ) {
80+ if ( elem . cld_image ) {
81+ // Use safer selector approach
82+ $ ( 'img' ) . filter ( function ( ) {
83+ return $ ( this ) . attr ( 'src' ) === elem . cld_image ;
84+ } ) . attr ( 'src' , elem . remote_image ) ;
85+ }
86+ } ) ;
87+ } ,
88+
89+ /**
90+ * Validate image URL
91+ * @param {string } url - URL to validate
92+ * @returns {boolean } Whether URL is valid
93+ */
94+ isValidImageUrl : function ( url ) {
95+ if ( ! url || typeof url !== 'string' ) {
96+ return false ;
97+ }
98+
99+ // Basic URL validation
100+ try {
101+ let urlObj = new URL ( url , window . location . origin ) ;
102+ return / \. ( j p g | j p e g | p n g | g i f | w e b p | s v g ) $ / i. test ( urlObj . pathname ) ||
103+ url . indexOf ( 'media/' ) !== - 1 ;
104+ } catch ( e ) {
105+ return false ;
106+ }
107+ } ,
108+
109+ /**
110+ * Update image with Cloudinary version
111+ * @param {string } key - Image key
112+ */
113+ update : function ( key ) {
114+ let self = this ,
115+ imageData = self . images [ key ] ;
116+
117+ // Validation checks
118+ if ( ! imageData || ! imageData . remote_image ) {
119+ return ;
120+ }
121+
122+ if ( ! self . isValidImageUrl ( imageData . remote_image ) ) {
123+ console . warn ( 'Invalid image URL:' , imageData . remote_image ) ;
124+ return ;
125+ }
126+
127+ // Check if already processing this image URL
128+ let imageUrl = imageData . remote_image ;
129+ if ( self . processedImages . has ( imageUrl ) ) {
130+ return ;
131+ }
132+
133+ // Abort previous request for this key if still pending
134+ if ( self . pendingRequests [ key ] ) {
135+ self . pendingRequests [ key ] . abort ( ) ;
136+ }
137+
138+ // Mark as being processed
139+ self . processedImages . add ( imageUrl ) ;
140+
141+ // Make AJAX request and store reference
142+ self . pendingRequests [ key ] = $ . ajax ( {
143+ url : config . ajaxUrl ,
144+ type : 'POST' ,
145+ dataType : 'json' ,
146+ data : {
147+ remote_image : imageData . remote_image ,
148+ form_key : window . FORM_KEY || ''
149+ } ,
150+ success : function ( image ) {
151+
152+ delete self . pendingRequests [ key ] ;
153+
154+ // Validate response
155+ if ( ! image || typeof image !== 'string' ) {
156+ console . warn ( 'Invalid response for image:' , key ) ;
157+ return ;
47158 }
48- } ) ;
49159
50- } )
160+ self . images [ key ] . cld_image = image ;
161+
162+ // Update image in DOM using safer selector
163+ $ ( 'img' ) . filter ( function ( ) {
164+ return $ ( this ) . attr ( 'src' ) === imageData . remote_image ;
165+ } ) . attr ( 'src' , image ) ;
166+ } ,
167+ error : function ( xhr , textStatus , errorThrown ) {
168+ // Clean up pending request reference
169+ delete self . pendingRequests [ key ] ;
170+
171+ // Only log if not aborted
172+ if ( textStatus !== 'abort' ) {
173+ console . error ( 'Cloudinary image update failed:' , {
174+ key : key ,
175+ status : textStatus ,
176+ error : errorThrown ,
177+ response : xhr . responseText
178+ } ) ;
179+
180+ // Remove from processed set to allow retry
181+ self . processedImages . delete ( imageUrl ) ;
182+ }
183+ }
184+ } ) ;
185+ } ,
186+
187+ /**
188+ * Cleanup event listeners and abort pending requests
189+ */
190+ destroy : function ( ) {
191+ let self = this ;
192+
193+ // Remove event listeners
194+ self . eventHandlers . forEach ( function ( handler ) {
195+ if ( handler . selector ) {
196+ $ ( handler . selector ) . off ( handler . event , handler . handler ) ;
197+ } else if ( handler . event ) {
198+ _PBEvents . off ( handler . event , handler . handler ) ;
199+ }
200+ } ) ;
201+
202+ // Abort all pending AJAX requests
203+ $ . each ( self . pendingRequests , function ( _ , request ) {
204+ request . abort ( ) ;
205+ } ) ;
206+
207+ // Clear data
208+ self . images = { } ;
209+ self . pendingRequests = { } ;
210+ self . processedImages . clear ( ) ;
211+ self . eventHandlers = [ ] ;
51212 }
52213 } ;
214+
53215 return updateHandler . init ( ) ;
54- }
55- } ) ;
216+ } ;
217+ } ) ;
0 commit comments