PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
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.83, at includes/admin/js/ai-imagefield.js

400 lines 21.6 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>' + ((window.KingAddonsAiImageField && KingAddonsAiImageField.missing_key_message) || 'The AI 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 the configured provider and default
114 var $modelSelect = $('<select class="kng-ai-image-model" style="margin-right:6px;"></select>');
115 var imageModels = KingAddonsAiImageField.image_models;
116 if (!Array.isArray(imageModels) || !imageModels.length) {
117 imageModels = [
118 { value: 'dall-e-3', label: 'DALL\u00b7E 3' },
119 { value: 'gpt-image-1', label: 'GPT Image 1' }
120 ];
121 }
122 imageModels.forEach(function(m) {
123 $modelSelect.append($('<option></option>').val(m.value).text(m.label));
124 });
125 // Apply default from settings
126 if (KingAddonsAiImageField.image_model) {
127 $modelSelect.val(KingAddonsAiImageField.image_model);
128 }
129 if (!$modelSelect.val() && imageModels.length) {
130 $modelSelect.val(imageModels[0].value);
131 }
132 // Define controls per model
133 var modelControls = {
134 'dall-e-3': {
135 qualities: [
136 { value: 'hd', label: 'HD (default)' },
137 { value: 'standard', label: 'Standard' }
138 ],
139 sizes: [
140 { value: '1024x1024', label: '1024×1024 (square)' },
141 { value: '1024x1792', label: '1024×1792 (portrait)' },
142 { value: '1792x1024', label: '1792×1024 (landscape)' }
143 ]
144 },
145 'gpt-image-1': {
146 qualities: [
147 { value: 'high', label: 'High (default)' },
148 { value: 'medium', label: 'Medium' },
149 { value: 'low', label: 'Low' },
150 { value: 'auto', label: 'Auto' },
151 ],
152 sizes: [
153 { value: 'auto', label: 'Auto (default)' },
154 { value: '1024x1024', label: '1024×1024 (square)' },
155 { value: '1536x1024', label: '1536×1024 (landscape)' },
156 { value: '1024x1536', label: '1024×1536 (portrait)' }
157 ]
158 }
159 };
160 // Function to update selects based on chosen model. Models that
161 // are not OpenAI's own (for example anything routed through
162 // OpenRouter) take no quality/size arguments, so those rows are
163 // hidden rather than sending options the provider will reject.
164 function updateControlsByModel() {
165 var m = $modelSelect.val();
166 var controls = modelControls[m];
167
168 if (!controls) {
169 $quality.empty().closest('.kng-ai-image-field-row').hide();
170 $resolution.empty().closest('.kng-ai-image-field-row').hide();
171 return;
172 }
173
174 $quality.closest('.kng-ai-image-field-row').show();
175 $resolution.closest('.kng-ai-image-field-row').show();
176
177 // update quality options
178 $quality.empty();
179 controls.qualities.forEach(function(opt) {
180 $quality.append('<option value="'+opt.value+'">'+opt.label+'</option>');
181 });
182 // update size options
183 $resolution.empty();
184 controls.sizes.forEach(function(opt) {
185 $resolution.append('<option value="'+opt.value+'">'+opt.label+'</option>');
186 });
187 }
188 // Bind change handler and initialize
189 $modelSelect.on('change', updateControlsByModel);
190 updateControlsByModel();
191 // Create transparent background checkbox (only for GPT Image 1)
192 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>');
193 // Update show/hide of background checkbox
194 function updateBgCheckbox() {
195 if ($modelSelect.val() === 'gpt-image-1') {
196 $bgCheckbox.show();
197 } else {
198 $bgCheckbox.hide().find('input').prop('checked', false);
199 }
200 }
201 $modelSelect.on('change', updateBgCheckbox);
202 updateBgCheckbox();
203 // Prompt submit button with icon
204 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'});
205 // Prompt cancel button with cross icon
206 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>');
207 // Append input and labelled rows for each control and buttons
208 $promptContainer.append(
209 $input,
210 $('<div class="kng-ai-image-field-row"></div>').append(
211 '<label class="kng-ai-image-field-label">Quality</label>',
212 $quality
213 ),
214 $('<div class="kng-ai-image-field-row"></div>').append(
215 '<label class="kng-ai-image-field-label">Size</label>',
216 $resolution
217 ),
218 $('<div class="kng-ai-image-field-row"></div>').append(
219 '<label class="kng-ai-image-field-label">Model</label>',
220 $modelSelect
221 ),
222 $('<div class="kng-ai-image-field-row"></div>').append($bgCheckbox),
223 $('<div class="kng-ai-image-field-row kng-ai-image-field-row--buttons"></div>').append(
224 $submit,
225 $cancel
226 )
227 );
228 $outer.append($promptContainer);
229 // Re-run now that the rows exist, so a model without quality /
230 // size options can hide them.
231 updateControlsByModel();
232 // Add user note about processing time
233 $promptContainer.append($('<p class="kng-ai-image-prompt-note">Complex prompts may take up to 2 minutes to process.</p>'));
234 // Mark the Generate Image button as active (container open)
235 $btn.addClass('is-active');
236
237 $submit.on('click', function() {
238 var prompt = $input.val().trim();
239 if (!prompt) { $input.css('border-color','red'); return; }
240 var selectedResolution = $resolution.val();
241 var selectedQuality = $quality.val();
242 var selectedModel = $modelSelect.val();
243 // Disable UI and animate while processing
244 $submit.prop('disabled', true).addClass('is-processing');
245 // Change button text preserving icon
246 $submit.find('span').text('Generating...');
247 // Animate image preview border
248 var $preview = $ctrl.find('.elementor-control-media__preview');
249 if ($preview.length) {
250 $preview.addClass('king-addons-field-pulsing');
251 }
252 // AJAX request
253 $.post(KingAddonsAiImageField.ajax_url, {
254 action: KingAddonsAiImageField.generate_action,
255 nonce: KingAddonsAiImageField.generate_nonce,
256 prompt: prompt,
257 quality: selectedQuality,
258 size: selectedResolution,
259 model: selectedModel,
260 background: $bgCheckbox.find('input').is(':checked') ? 'transparent' : '',
261 }).done(function(response) {
262 if (response.success && response.data.attachment_id) {
263 var attachmentID = response.data.attachment_id;
264 var imageURL = response.data.url;
265
266 // Get the current element being edited
267 var currentElement = elementor.getPanelView().getCurrentPageView().getOption('editedElementView');
268 if (currentElement) {
269 // Get the control name from the hidden input
270 var controlName = $ctrl.find('input[type="hidden"][data-setting]').data('setting');
271 if (controlName) {
272 // Create the image data object
273 var imageData = {
274 id: attachmentID,
275 url: imageURL
276 };
277
278 // Update the element's settings
279 var settings = {};
280 settings[controlName] = imageData;
281
282 // Use $e.run to properly update the element
283 $e.run('document/elements/settings', {
284 container: currentElement.getContainer(),
285 settings: settings
286 });
287
288 // Update the control preview
289 if ($preview.length) {
290 $preview.css('background-image', 'url(' + imageURL + ')');
291 }
292 }
293 }
294 } else {
295 alert('Error generating image. ' + response.responseJSON.data.message);
296 }
297 }).fail(function(response) {
298 alert('Error generating image. ' + response.responseJSON.data.message);
299 }).always(function() {
300 // Clean up animations and UI
301 $submit.prop('disabled', false).removeClass('is-processing').find('span').text('Generate');
302 if (typeof $preview !== 'undefined' && $preview.length) {
303 $preview.removeClass('king-addons-field-pulsing');
304 }
305 // Remove active state on complete
306 $btn.removeClass('is-active');
307 $promptContainer.remove();
308 });
309 });
310
311 $cancel.on('click', function() {
312 // Remove active state when closing container
313 $btn.removeClass('is-active');
314 $promptContainer.remove();
315 });
316 });
317 });
318 }
319
320 // Global observer: watch for any media control fields added anywhere
321 var __kngGlobalObserver = new MutationObserver(function(mutations) {
322 mutations.forEach(function(mutation) {
323 Array.prototype.forEach.call(mutation.addedNodes, function(node) {
324 if (node.nodeType !== 1) {
325 return;
326 }
327 var $node = $(node);
328 // Check for media field or containing media field
329 var $mediaField = $node.is('.elementor-control-field.elementor-control-media') ? $node : $node.find('.elementor-control-field.elementor-control-media');
330 if (!$mediaField.length) {
331 return;
332 }
333 // Find the parent control wrapper and its panel
334 var $ctrl = $mediaField.closest('.elementor-control.elementor-control-type-media, .elementor-control.elementor-control-type-image, .elementor-control.elementor-control-type-gallery');
335 if (!$ctrl.length) {
336 return;
337 }
338 var $panel = $ctrl.closest('.elementor-panel');
339 if ($panel.length) {
340 injectImageControls($panel);
341 }
342 });
343 });
344 });
345 __kngGlobalObserver.observe(document.body, { childList: true, subtree: true });
346
347 // On widget panel open, inject image controls and watch for dynamically added controls
348 elementor.hooks.addAction('panel/open_editor/widget', function(panel) {
349 var $panelEl = panel.$el;
350 // Initial injection after panel renders
351 setTimeout(function() {
352 injectImageControls($panelEl);
353 }, 250);
354 // Setup a MutationObserver to catch lazy-loaded controls
355 var root = $panelEl[0];
356 if (!root.__kngAiObserver) {
357 var observer = new MutationObserver(function(mutations) {
358 mutations.forEach(function(mutation) {
359 Array.prototype.forEach.call(mutation.addedNodes, function(node) {
360 if (node.nodeType === 1 && (node.matches('.elementor-control.elementor-control-type-media, .elementor-control.elementor-control-type-image, .elementor-control.elementor-control-type-gallery') ||
361 $(node).find('.elementor-control.elementor-control-type-media, .elementor-control.elementor-control-type-image, .elementor-control.elementor-control-type-gallery').length)) {
362 injectImageControls($panelEl);
363 }
364 });
365 });
366 });
367 observer.observe(root, { childList: true, subtree: true });
368 root.__kngAiObserver = observer;
369 }
370 // Extra delayed injections to catch any late-rendered controls
371 setTimeout(function() { injectImageControls($panelEl); }, 800);
372 setTimeout(function() { injectImageControls($panelEl); }, 1500);
373 });
374
375 // Also monitor section changes (e.g., switching tabs)
376 elementor.channels.editor.on('section:activated', function(sectionName, editor) {
377 var panelView = editor.getOption('editedElementView').getContainer().panel;
378 if (panelView && panelView.$el) {
379 setTimeout(function() {
380 injectImageControls(panelView.$el);
381 }, 150);
382 }
383 });
384
385 // Re-inject image controls whenever a control view is opened
386 elementor.hooks.addAction('panel/open_editor/control', function(controlView) {
387 var panelEl = controlView.$el.closest('.elementor-panel');
388 if (panelEl.length) {
389 injectImageControls(panelEl);
390 }
391 });
392
393 // Listen for Background Type toggles and re-inject Generate Image buttons when sub-controls appear
394 $(document).on('change', '.elementor-control.elementor-control-background_background input[type="radio"]', function() {
395 var panelEl = $(this).closest('.elementor-panel');
396 setTimeout(function() {
397 injectImageControls(panelEl);
398 }, 100);
399 });
400 })(jQuery, window.elementor);