PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.78
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.78
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / admin / js / ai-imagefield.js

ai-imagefield.js in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.78, at includes/admin/js/ai-imagefield.js

377 lines 20.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function($, elementor) {
2 'use strict';
3
4 // Ensure AI field localization exists
5 if (!window.KingAddonsAiImageField) {
6 return;
7 }
8 var data = window.KingAddonsAiImageField;
9
10 /**
11 * Injects the Generate Image button and prompt UI into media controls
12 * @param {jQuery} $container Elementor panel container
13 */
14 function injectImageControls($container) {
15 $container.find(
16 '.elementor-control.elementor-control-type-media, '
17 + '.elementor-control.elementor-control-type-image, '
18 + '.elementor-control.elementor-control-type-gallery'
19 ).each(function() {
20 var $ctrl = $(this);
21 // Skip if already injected
22 if ($ctrl.find('.kng-ai-image-btn-wrapper').length) {
23 return;
24 }
25 // Find the label of the media field (not nested labels like Resolution)
26 var $mediaTitle = $ctrl.find('.elementor-control-field.elementor-control-media > .elementor-control-title').first();
27 if (!$mediaTitle.length) {
28 // fallback to any control title
29 $mediaTitle = $ctrl.find('.elementor-control-title').first();
30 }
31 // Wrap button and prompt in container
32 var $outer = $('<div class="kng-ai-image-field-wrapper"></div>');
33 // Insert button/prompt wrapper after the input wrapper if available, otherwise after the title
34 var $inputWrapper = $ctrl.find('.elementor-control-input-wrapper').first();
35 if ($inputWrapper.length) {
36 $inputWrapper.after($outer);
37 } else {
38 $mediaTitle.after($outer);
39 }
40 var $wrapper = $('<div class="kng-ai-image-btn-wrapper" style="margin-top:8px;"></div>');
41 // Button with icon, matching AI text feature
42 var $btn = $('<button type="button" class="kng-ai-image-generate-btn elementor-button elementor-size-sm"><img src="' + data.icon_url + '" style="width:16px;height:16px;margin-right:6px;vertical-align:middle;"/><span>Generate Image</span></button>');
43 $outer.append($wrapper);
44 $wrapper.append($btn);
45
46 $btn.on('click', function(e) {
47 e.preventDefault();
48 // Remove existing prompt UI
49 $outer.find('.kng-ai-image-prompt-container').remove();
50
51 // FIRST_EDIT: Check API key validity before proceeding
52 var apiKeyValid = true;
53 $.ajax({
54 url: KingAddonsAiImageField.ajax_url,
55 method: 'POST',
56 async: false,
57 dataType: 'json',
58 data: {
59 action: 'king_addons_ai_image_check_limits',
60 nonce: KingAddonsAiImageField.generate_nonce
61 },
62 success: function(resp) {
63 apiKeyValid = resp.success && resp.data.api_key_valid === true;
64 },
65 error: function() {
66 apiKeyValid = true; // on error assume valid
67 }
68 });
69 if (!apiKeyValid) {
70 var settingsUrl = window.KingAddonsAiImageField && window.KingAddonsAiImageField.settings_url
71 ? window.KingAddonsAiImageField.settings_url
72 : '/wp-admin/admin.php?page=king-addons-ai-settings';
73 var $errorMessage = $('<div class="kng-ai-image-error-message"></div>').css({
74 background: '#e7f3fe',
75 color: '#084d7a',
76 padding: '10px 15px',
77 borderRadius: '4px',
78 border: '1px solid #b6e0fe',
79 margin: '8px 0',
80 fontSize: '13px',
81 fontWeight: '500',
82 display: 'flex',
83 alignItems: 'center',
84 justifyContent: 'space-between'
85 });
86 $errorMessage.html(
87 '<span>OpenAI API key is missing or invalid. Please configure your API key in AI Settings.</span>' +
88 '<a href="' + settingsUrl + '" style="color:#0073aa;text-decoration:underline;white-space:nowrap;margin-left:10px;" target="_blank">Settings</a>'
89 );
90 $wrapper.after($errorMessage).hide().fadeIn(200);
91 setTimeout(function() { $errorMessage.fadeOut(200, function() { $(this).remove(); }); }, 5000);
92 return;
93 }
94
95 // Proceed to show prompt UI
96 // Create prompt UI
97 var $promptContainer = $('<div class="kng-ai-image-prompt-container"></div>').css({margin:'8px 0'});
98 var $input = $('<input type="text" class="kng-ai-image-prompt-input" placeholder="Enter image prompt..." style="width:100%;padding:6px 10px;border:1px solid #ccc;border-radius:4px;margin-bottom:6px;"/>');
99 var $quality = $('<select class="kng-ai-image-quality" style="margin-right:6px;"></select>');
100 ['hd','standard'].forEach(function(q) {
101 $quality.append('<option value="'+q+'">'+q.charAt(0).toUpperCase()+q.slice(1)+'</option>');
102 });
103 var $resolution = $('<select class="kng-ai-image-resolution" style="margin-right:6px;"></select>');
104 // Define resolutions with display labels
105 var resolutions = [
106 { value: '1024x1024', label: '1024×1024 (square)' },
107 { value: '1024x1792', label: '1024×1792 (portrait)' },
108 { value: '1792x1024', label: '1792×1024 (landscape)' }
109 ];
110 resolutions.forEach(function(res) {
111 $resolution.append('<option value="' + res.value + '">' + res.label + '</option>');
112 });
113 // Model selector reflecting saved default
114 var $modelSelect = $('<select class="kng-ai-image-model" style="margin-right:6px;"></select>');
115 var imageModels = [
116 { value: 'dall-e-3', label: 'DALL·E 3' },
117 { value: 'gpt-image-1', label: 'GPT Image 1' }
118 ];
119 imageModels.forEach(function(m) {
120 $modelSelect.append('<option value="' + m.value + '">' + m.label + '</option>');
121 });
122 // Apply default from settings
123 if (KingAddonsAiImageField.image_model) {
124 $modelSelect.val(KingAddonsAiImageField.image_model);
125 }
126 // Define controls per model
127 var modelControls = {
128 'dall-e-3': {
129 qualities: [
130 { value: 'hd', label: 'HD (default)' },
131 { value: 'standard', label: 'Standard' }
132 ],
133 sizes: [
134 { value: '1024x1024', label: '1024×1024 (square)' },
135 { value: '1024x1792', label: '1024×1792 (portrait)' },
136 { value: '1792x1024', label: '1792×1024 (landscape)' }
137 ]
138 },
139 'gpt-image-1': {
140 qualities: [
141 { value: 'high', label: 'High (default)' },
142 { value: 'medium', label: 'Medium' },
143 { value: 'low', label: 'Low' },
144 { value: 'auto', label: 'Auto' },
145 ],
146 sizes: [
147 { value: 'auto', label: 'Auto (default)' },
148 { value: '1024x1024', label: '1024×1024 (square)' },
149 { value: '1536x1024', label: '1536×1024 (landscape)' },
150 { value: '1024x1536', label: '1024×1536 (portrait)' }
151 ]
152 }
153 };
154 // Function to update selects based on chosen model
155 function updateControlsByModel() {
156 var m = $modelSelect.val();
157 // update quality options
158 $quality.empty();
159 modelControls[m].qualities.forEach(function(opt) {
160 $quality.append('<option value="'+opt.value+'">'+opt.label+'</option>');
161 });
162 // update size options
163 $resolution.empty();
164 modelControls[m].sizes.forEach(function(opt) {
165 $resolution.append('<option value="'+opt.value+'">'+opt.label+'</option>');
166 });
167 }
168 // Bind change handler and initialize
169 $modelSelect.on('change', updateControlsByModel);
170 updateControlsByModel();
171 // Create transparent background checkbox (only for GPT Image 1)
172 var $bgCheckbox = $('<label class="kng-ai-image-bg-checkbox-label" style="margin-right: 6px;margin-bottom: 8px;margin-top: 8px; display:none;"><input type="checkbox" class="kng-ai-image-bg-checkbox" style="margin-right: 8px; border-color: #ccc;"/>Transparent background</label>');
173 // Update show/hide of background checkbox
174 function updateBgCheckbox() {
175 if ($modelSelect.val() === 'gpt-image-1') {
176 $bgCheckbox.show();
177 } else {
178 $bgCheckbox.hide().find('input').prop('checked', false);
179 }
180 }
181 $modelSelect.on('change', updateBgCheckbox);
182 updateBgCheckbox();
183 // Prompt submit button with icon
184 var $submit = $('<button type="button" class="kng-ai-image-prompt-submit elementor-button elementor-button-primary elementor-size-sm"><img src="' + data.icon_url + '" style="width:16px;height:16px;margin-right:6px;vertical-align:middle;"/><span>Generate</span></button>').css({marginRight:'6px'});
185 // Prompt cancel button with cross icon
186 var $cancel = $('<button type="button" class="kng-ai-image-prompt-cancel elementor-button elementor-button-link elementor-size-sm" title="Cancel"><span class="kng-ai-image-cancel-icon">✕</span></button>');
187 // Append input and labelled rows for each control and buttons
188 $promptContainer.append(
189 $input,
190 $('<div class="kng-ai-image-field-row"></div>').append(
191 '<label class="kng-ai-image-field-label">Quality</label>',
192 $quality
193 ),
194 $('<div class="kng-ai-image-field-row"></div>').append(
195 '<label class="kng-ai-image-field-label">Size</label>',
196 $resolution
197 ),
198 $('<div class="kng-ai-image-field-row"></div>').append(
199 '<label class="kng-ai-image-field-label">Model</label>',
200 $modelSelect
201 ),
202 $('<div class="kng-ai-image-field-row"></div>').append($bgCheckbox),
203 $('<div class="kng-ai-image-field-row kng-ai-image-field-row--buttons"></div>').append(
204 $submit,
205 $cancel
206 )
207 );
208 $outer.append($promptContainer);
209 // Add user note about processing time
210 $promptContainer.append($('<p class="kng-ai-image-prompt-note">Complex prompts may take up to 2 minutes to process.</p>'));
211 // Mark the Generate Image button as active (container open)
212 $btn.addClass('is-active');
213
214 $submit.on('click', function() {
215 var prompt = $input.val().trim();
216 if (!prompt) { $input.css('border-color','red'); return; }
217 var selectedResolution = $resolution.val();
218 var selectedQuality = $quality.val();
219 var selectedModel = $modelSelect.val();
220 // Disable UI and animate while processing
221 $submit.prop('disabled', true).addClass('is-processing');
222 // Change button text preserving icon
223 $submit.find('span').text('Generating...');
224 // Animate image preview border
225 var $preview = $ctrl.find('.elementor-control-media__preview');
226 if ($preview.length) {
227 $preview.addClass('king-addons-field-pulsing');
228 }
229 // AJAX request
230 $.post(KingAddonsAiImageField.ajax_url, {
231 action: KingAddonsAiImageField.generate_action,
232 nonce: KingAddonsAiImageField.generate_nonce,
233 prompt: prompt,
234 quality: selectedQuality,
235 size: selectedResolution,
236 model: selectedModel,
237 background: $bgCheckbox.find('input').is(':checked') ? 'transparent' : '',
238 }).done(function(response) {
239 if (response.success && response.data.attachment_id) {
240 var attachmentID = response.data.attachment_id;
241 var imageURL = response.data.url;
242
243 // Get the current element being edited
244 var currentElement = elementor.getPanelView().getCurrentPageView().getOption('editedElementView');
245 if (currentElement) {
246 // Get the control name from the hidden input
247 var controlName = $ctrl.find('input[type="hidden"][data-setting]').data('setting');
248 if (controlName) {
249 // Create the image data object
250 var imageData = {
251 id: attachmentID,
252 url: imageURL
253 };
254
255 // Update the element's settings
256 var settings = {};
257 settings[controlName] = imageData;
258
259 // Use $e.run to properly update the element
260 $e.run('document/elements/settings', {
261 container: currentElement.getContainer(),
262 settings: settings
263 });
264
265 // Update the control preview
266 if ($preview.length) {
267 $preview.css('background-image', 'url(' + imageURL + ')');
268 }
269 }
270 }
271 } else {
272 alert('Error generating image. ' + response.responseJSON.data.message);
273 }
274 }).fail(function(response) {
275 alert('Error generating image. ' + response.responseJSON.data.message);
276 }).always(function() {
277 // Clean up animations and UI
278 $submit.prop('disabled', false).removeClass('is-processing').find('span').text('Generate');
279 if (typeof $preview !== 'undefined' && $preview.length) {
280 $preview.removeClass('king-addons-field-pulsing');
281 }
282 // Remove active state on complete
283 $btn.removeClass('is-active');
284 $promptContainer.remove();
285 });
286 });
287
288 $cancel.on('click', function() {
289 // Remove active state when closing container
290 $btn.removeClass('is-active');
291 $promptContainer.remove();
292 });
293 });
294 });
295 }
296
297 // Global observer: watch for any media control fields added anywhere
298 var __kngGlobalObserver = new MutationObserver(function(mutations) {
299 mutations.forEach(function(mutation) {
300 Array.prototype.forEach.call(mutation.addedNodes, function(node) {
301 if (node.nodeType !== 1) {
302 return;
303 }
304 var $node = $(node);
305 // Check for media field or containing media field
306 var $mediaField = $node.is('.elementor-control-field.elementor-control-media') ? $node : $node.find('.elementor-control-field.elementor-control-media');
307 if (!$mediaField.length) {
308 return;
309 }
310 // Find the parent control wrapper and its panel
311 var $ctrl = $mediaField.closest('.elementor-control.elementor-control-type-media, .elementor-control.elementor-control-type-image, .elementor-control.elementor-control-type-gallery');
312 if (!$ctrl.length) {
313 return;
314 }
315 var $panel = $ctrl.closest('.elementor-panel');
316 if ($panel.length) {
317 injectImageControls($panel);
318 }
319 });
320 });
321 });
322 __kngGlobalObserver.observe(document.body, { childList: true, subtree: true });
323
324 // On widget panel open, inject image controls and watch for dynamically added controls
325 elementor.hooks.addAction('panel/open_editor/widget', function(panel) {
326 var $panelEl = panel.$el;
327 // Initial injection after panel renders
328 setTimeout(function() {
329 injectImageControls($panelEl);
330 }, 250);
331 // Setup a MutationObserver to catch lazy-loaded controls
332 var root = $panelEl[0];
333 if (!root.__kngAiObserver) {
334 var observer = new MutationObserver(function(mutations) {
335 mutations.forEach(function(mutation) {
336 Array.prototype.forEach.call(mutation.addedNodes, function(node) {
337 if (node.nodeType === 1 && (node.matches('.elementor-control.elementor-control-type-media, .elementor-control.elementor-control-type-image, .elementor-control.elementor-control-type-gallery') ||
338 $(node).find('.elementor-control.elementor-control-type-media, .elementor-control.elementor-control-type-image, .elementor-control.elementor-control-type-gallery').length)) {
339 injectImageControls($panelEl);
340 }
341 });
342 });
343 });
344 observer.observe(root, { childList: true, subtree: true });
345 root.__kngAiObserver = observer;
346 }
347 // Extra delayed injections to catch any late-rendered controls
348 setTimeout(function() { injectImageControls($panelEl); }, 800);
349 setTimeout(function() { injectImageControls($panelEl); }, 1500);
350 });
351
352 // Also monitor section changes (e.g., switching tabs)
353 elementor.channels.editor.on('section:activated', function(sectionName, editor) {
354 var panelView = editor.getOption('editedElementView').getContainer().panel;
355 if (panelView && panelView.$el) {
356 setTimeout(function() {
357 injectImageControls(panelView.$el);
358 }, 150);
359 }
360 });
361
362 // Re-inject image controls whenever a control view is opened
363 elementor.hooks.addAction('panel/open_editor/control', function(controlView) {
364 var panelEl = controlView.$el.closest('.elementor-panel');
365 if (panelEl.length) {
366 injectImageControls(panelEl);
367 }
368 });
369
370 // Listen for Background Type toggles and re-inject Generate Image buttons when sub-controls appear
371 $(document).on('change', '.elementor-control.elementor-control-background_background input[type="radio"]', function() {
372 var panelEl = $(this).closest('.elementor-panel');
373 setTimeout(function() {
374 injectImageControls(panelEl);
375 }, 100);
376 });
377 })(jQuery, window.elementor);