PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.87
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.87
51.1.87 51.1.86 51.1.84 51.1.85 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 All 41 releases
← All changes | includes/admin/js/ai-imagefield.js +400 -0 51.1.2 → 51.1.87 View file →
@@ -1,0 +1,400 @@
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);