PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / schema-markup / js / schema-markup-admin.js

schema-markup-admin.js in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at schema-markup/js/schema-markup-admin.js

802 lines 28.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Schema Markup Admin JavaScript
3 *
4 * @package MetaSync
5 * @subpackage Schema_Markup
6 */
7
8 /* global metasyncSchemaMarkup */
9 jQuery(document).ready(function ($) {
10 let schemaTypeCounter = 0;
11
12 // Initialize counter based on existing schema types
13 function initializeSchemaTypeCounter() {
14 const existingItems = $('.schema-type-item');
15 if (existingItems.length > 0) {
16 let maxIndex = -1;
17 existingItems.each(function () {
18 const index = parseInt($(this).attr('data-index'));
19 if (index > maxIndex) {
20 maxIndex = index;
21 }
22 });
23 schemaTypeCounter = maxIndex + 1;
24 }
25 }
26
27 // Initialize counter when document is ready
28 initializeSchemaTypeCounter();
29
30 // Function to check if a schema type already exists
31 function schemaTypeExists(schemaType) {
32 let exists = false;
33 $('.schema-type-item').each(function () {
34 const existingType = $(this).find('input[name*="[type]"]').val();
35 if (existingType === schemaType) {
36 exists = true;
37 return false; // break loop
38 }
39 });
40 return exists;
41 }
42
43 // Function to add a new schema type
44 function addSchemaType(schemaType) {
45 // Check if this schema type already exists
46 if (schemaTypeExists(schemaType)) {
47 return;
48 }
49
50 const index = schemaTypeCounter++;
51 const schemaTypeNames = {
52 'article': 'Article',
53 'FAQPage': 'FAQ',
54 'product': 'Product',
55 'recipe': 'Recipe',
56 'Event': 'Event',
57 'JobPosting': 'Job Posting',
58 'Review': 'Review',
59 'Course': 'Course',
60 'Organization': 'Organization',
61 'Person': 'Person',
62 'WebSite': 'Website',
63 'NewsArticle': 'News Article',
64 'LocalBusiness': 'Local Business',
65 'HowTo': 'How-To',
66 'VideoObject': 'Video Object'
67 };
68 const displayName = schemaTypeNames[schemaType] || schemaType;
69
70 const schemaTypeHtml = '<div class="schema-type-item" data-index="' + index + '" style="margin: 15px 0; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">' +
71 '<div class="schema-type-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">' +
72 '<h5 style="margin: 0; color: #333;">' +
73 '<span class="dashicons dashicons-tag" style="color: #0073aa;"></span> ' + displayName + ' Schema' +
74 '</h5>' +
75 '<button type="button" class="button button-link remove-schema-type" style="color: #dc3232; text-decoration: none;">' +
76 '<span class="dashicons dashicons-trash"></span> Remove' +
77 '</button>' +
78 '</div>' +
79 '<div class="schema-type-content">' +
80 '<input type="hidden" name="schema_markup[types][' + index + '][type]" value="' + schemaType + '">' +
81 '<div class="schema-fields-container">' +
82 '</div>' +
83 '</div>' +
84 '</div>';
85
86 // Remove no-schema-types message if it exists
87 $('.no-schema-types').remove();
88
89 // Add the new schema type
90 $('#schema-types-list').append(schemaTypeHtml);
91
92 // Load the fields for this schema type
93 loadSchemaFields(schemaType, index);
94 }
95
96 // Function to load schema fields via AJAX
97 function loadSchemaFields(schemaType, index) {
98 const postId = $('#post_ID').val() || 0;
99 $.ajax({
100 url: metasyncSchemaMarkup.ajaxurl,
101 type: 'POST',
102 data: {
103 action: 'metasync_get_schema_fields',
104 schema_type: schemaType,
105 index: index,
106 post_id: postId,
107 nonce: metasyncSchemaMarkup.schema_fields_nonce
108 },
109 success: function (response) {
110 if (response.success) {
111 var fieldsContainer = $('.schema-type-item[data-index="' + index + '"] .schema-fields-container');
112 fieldsContainer.empty();
113 var parser = new DOMParser();
114 var doc = parser.parseFromString(response.data || '', 'text/html');
115 while (doc.body.firstChild) {
116 fieldsContainer[0].appendChild(doc.body.firstChild);
117 }
118 }
119 }
120 });
121 }
122
123 // Toggle schema fields based on enabled checkbox
124 $('input[name="schema_markup[enabled]"]').change(function () {
125 if ($(this).is(':checked')) {
126 $('.schema-types-container').show();
127 } else {
128 $('.schema-types-container').hide();
129 }
130 });
131
132 // Handle modal cancel button
133 $(document).on('click', '#cancel-schema-type', function () {
134 $('#schema-type-modal').remove();
135 });
136
137 // Handle modal add button
138 $(document).on('click', '#add-selected-schema-type', function () {
139 const selectedType = $('#new-schema-type-select').val();
140 const selectedOption = $('#new-schema-type-select option:selected');
141
142 if (!selectedType) {
143 alert('Please select a schema type.');
144 return;
145 }
146
147 // Check if the selected option is disabled
148 if (selectedOption.prop('disabled')) {
149 alert('This schema type has already been added. Please select a different schema type or remove the existing one first.');
150 return;
151 }
152
153 addSchemaType(selectedType);
154 $('#schema-type-modal').remove();
155 });
156
157 // Add new schema type
158 $(document).on('click', '#add_schema_type_button', function (e) {
159 e.preventDefault();
160
161 // Show schema type selection modal or dropdown
162 showSchemaTypeSelection();
163 });
164
165 // Remove schema type
166 $(document).on('click', '.remove-schema-type', function (e) {
167 e.preventDefault();
168
169 if (confirm('Are you sure you want to remove this schema type?')) {
170 $(this).closest('.schema-type-item').fadeOut(200, function () {
171 $(this).remove();
172 updateSchemaTypeIndices();
173 updateNoSchemaTypesMessage();
174 });
175 }
176 });
177
178 function showSchemaTypeSelection() {
179 const schemaTypes = [
180 { value: 'article', label: 'Article' },
181 { value: 'FAQPage', label: 'FAQ' },
182 { value: 'product', label: 'Product' },
183 { value: 'recipe', label: 'Recipe' },
184 { value: 'Event', label: 'Event' },
185 { value: 'JobPosting', label: 'Job Posting' },
186 { value: 'Review', label: 'Review' },
187 { value: 'Course', label: 'Course' },
188 { value: 'Organization', label: 'Organization' },
189 { value: 'Person', label: 'Person' },
190 { value: 'WebSite', label: 'Website' },
191 { value: 'NewsArticle', label: 'News Article' },
192 { value: 'LocalBusiness', label: 'Local Business' },
193 { value: 'HowTo', label: 'How-To' },
194 { value: 'VideoObject', label: 'Video Object' }
195 ];
196
197 let optionsHtml = '<option value="">Select Schema Type</option>';
198 schemaTypes.forEach(function (type) {
199 const isAlreadyAdded = schemaTypeExists(type.value);
200 let label = type.label;
201 let disabledAttr = '';
202
203 if (isAlreadyAdded) {
204 label += ' - already added';
205 disabledAttr = ' disabled';
206 }
207
208 optionsHtml += '<option value="' + type.value + '"' + disabledAttr + '>' + label + '</option>';
209 });
210
211 const modalHtml = '<div id="schema-type-modal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; display: flex; align-items: center; justify-content: center;">' +
212 '<div style="background: white; padding: 20px; border-radius: 4px; min-width: 300px;">' +
213 '<h3 style="margin-top: 0;">Add Schema Type</h3>' +
214 '<p>Select the schema type you want to add:</p>' +
215 '<select id="new-schema-type-select" style="width: 100%; margin-bottom: 8px;">' + optionsHtml + '</select>' +
216 '<p class="description" style="margin: 0 0 15px 0; color: #646970; font-size: 12px; font-style: italic;">' +
217 'Pick the type that best describes this page\'s main content — e.g. \'Product\' for a product page, \'FAQ\' for a Q&amp;A page, \'Article\' for a blog post. This tells Google how to display your page in search results.' +
218 '</p>' +
219 '<div style="text-align: right;">' +
220 '<button type="button" class="button" id="cancel-schema-type">Cancel</button> ' +
221 '<button type="button" class="button button-primary" id="add-selected-schema-type">Add Schema Type</button>' +
222 '</div>' +
223 '</div>' +
224 '</div>';
225
226 $('body').append(modalHtml);
227 }
228
229
230 function updateSchemaTypeIndices() {
231 $('.schema-type-item').each(function (newIndex) {
232 $(this).attr('data-index', newIndex);
233 $(this).find('input[name*="[type]"]').attr('name', 'schema_markup[types][' + newIndex + '][type]');
234
235 // Update all field names
236 $(this).find('[name*="schema_markup[types]"]').each(function () {
237 const name = $(this).attr('name');
238 const newName = name.replace(/schema_markup\[types\]\[\d+\]/, 'schema_markup[types][' + newIndex + ']');
239 $(this).attr('name', newName);
240 });
241 });
242
243 // Update counter to be the next available index
244 schemaTypeCounter = $('.schema-type-item').length;
245 }
246
247 function updateNoSchemaTypesMessage() {
248 if ($('.schema-type-item').length === 0) {
249 $('#schema-types-list').html('<div class="no-schema-types" style="text-align: center; padding: 20px; background: #f9f9f9; border: 2px dashed #ddd; border-radius: 4px; color: #666;"><p style="margin: 0;">No schema types added yet. Click "Add Schema Type" to get started.</p></div>');
250 }
251 }
252
253 // Add ingredient
254 $(document).on('click', '.add-ingredient', function () {
255 const container = $(this).siblings('.ingredients-list');
256 const schemaTypeItem = $(this).closest('.schema-type-item');
257 const schemaIndex = schemaTypeItem.attr('data-index');
258
259 const newItem = '<div class="ingredient-item">' +
260 '<input type="text" name="schema_markup[types][' + schemaIndex + '][fields][ingredients][]" placeholder="Enter ingredient">' +
261 '<button type="button" class="remove-item">Remove</button>' +
262 '</div>';
263 container.append(newItem);
264 });
265
266 // Add instruction
267 $(document).on('click', '.add-instruction', function () {
268 const container = $(this).siblings('.instructions-list');
269 const schemaTypeItem = $(this).closest('.schema-type-item');
270 const schemaIndex = schemaTypeItem.attr('data-index');
271
272 const newItem = '<div class="instruction-item">' +
273 '<textarea name="schema_markup[types][' + schemaIndex + '][fields][instructions][]" placeholder="Enter instruction step"></textarea>' +
274 '<button type="button" class="remove-item">Remove</button>' +
275 '</div>';
276 container.append(newItem);
277 });
278
279 // Remove item
280 $(document).on('click', '.remove-item', function () {
281 $(this).parent().remove();
282 });
283
284 // Media uploader for organization logo
285 let mediaUploader;
286
287 $(document).on('click', '.upload-logo-button', function (e) {
288 e.preventDefault();
289 const targetIndex = $(this).data('target');
290
291 // If the media frame already exists, reopen it
292 if (mediaUploader) {
293 mediaUploader.open();
294 return;
295 }
296
297 // Create the media frame
298 mediaUploader = wp.media({
299 title: 'Choose Organization Logo',
300 button: {
301 text: 'Use this logo'
302 },
303 multiple: false,
304 library: {
305 type: 'image'
306 }
307 });
308
309 // When an image is selected, run a callback
310 mediaUploader.on('select', function () {
311 const attachment = mediaUploader.state().get('selection').first().toJSON();
312 $('#article_organization_logo_' + targetIndex).val(attachment.url);
313 $('#logo_preview_' + targetIndex + ' img').attr('src', attachment.url);
314 $('#logo_preview_' + targetIndex).show();
315 $('.remove-logo-button[data-target="' + targetIndex + '"]').show();
316 });
317
318 // Open the uploader dialog
319 mediaUploader.open();
320 });
321
322 // Remove logo
323 $(document).on('click', '.remove-logo-button', function (e) {
324 e.preventDefault();
325 const targetIndex = $(this).data('target');
326 $('#article_organization_logo_' + targetIndex).val('');
327 $('#logo_preview_' + targetIndex).hide();
328 $(this).hide();
329 });
330
331 // Add opening hours item (LocalBusiness)
332 $(document).on('click', '.add-opening-hours', function (e) {
333 e.preventDefault();
334 const container = $(this).siblings('.opening-hours-list');
335 const count = container.find('.opening-hours-item').length;
336 const schemaIndex = $(this).data('index');
337
338 const newItem = '<div class="opening-hours-item" style="margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">' +
339 '<input type="text" name="schema_markup[types][' + schemaIndex + '][fields][opening_hours][' + count + '][day]" value="" placeholder="e.g., Monday" style="width: 30%;">' +
340 '<input type="text" name="schema_markup[types][' + schemaIndex + '][fields][opening_hours][' + count + '][open]" value="" placeholder="09:00" style="width: 25%;">' +
341 '<input type="text" name="schema_markup[types][' + schemaIndex + '][fields][opening_hours][' + count + '][close]" value="" placeholder="17:00" style="width: 25%;">' +
342 '<button type="button" class="button remove-item">Remove</button>' +
343 '</div>';
344 container.append(newItem);
345 });
346
347 // Add supply item (HowTo)
348 $(document).on('click', '.add-supply', function (e) {
349 e.preventDefault();
350 const container = $(this).siblings('.supplies-list');
351 const schemaIndex = $(this).data('index');
352
353 const newItem = '<div class="supply-item">' +
354 '<input type="text" name="schema_markup[types][' + schemaIndex + '][fields][supplies][]" placeholder="Enter supply">' +
355 '<button type="button" class="remove-item">Remove</button>' +
356 '</div>';
357 container.append(newItem);
358 });
359
360 // Add tool item (HowTo)
361 $(document).on('click', '.add-tool', function (e) {
362 e.preventDefault();
363 const container = $(this).siblings('.tools-list');
364 const schemaIndex = $(this).data('index');
365
366 const newItem = '<div class="tool-item">' +
367 '<input type="text" name="schema_markup[types][' + schemaIndex + '][fields][tools][]" placeholder="Enter tool">' +
368 '<button type="button" class="remove-item">Remove</button>' +
369 '</div>';
370 container.append(newItem);
371 });
372
373 // Add HowTo step
374 $(document).on('click', '.add-howto-step', function (e) {
375 e.preventDefault();
376 const container = $(this).siblings('.howto-steps-list');
377 const count = container.find('.howto-step-item').length;
378 const schemaIndex = $(this).data('index');
379
380 const newItem = '<div class="howto-step-item" style="margin: 15px 0; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">' +
381 '<div class="schema-field">' +
382 '<label>Step ' + (count + 1) + ' Instructions: <span style="color: #dc3232;">*</span></label>' +
383 '<textarea name="schema_markup[types][' + schemaIndex + '][fields][steps][' + count + '][instructions]" placeholder="Enter step instructions" style="width: 100%; height: 80px;"></textarea>' +
384 '</div>' +
385 '<div class="schema-field">' +
386 '<label>Step ' + (count + 1) + ' Image (optional):</label>' +
387 '<input type="url" name="schema_markup[types][' + schemaIndex + '][fields][steps][' + count + '][image]" value="" placeholder="https://example.com/step-image.jpg" style="width: 100%;">' +
388 '</div>' +
389 '<button type="button" class="button remove-howto-step" style="background: #dc3232; color: white; border-color: #dc3232;">Remove Step</button>' +
390 '</div>';
391 container.append(newItem);
392 });
393
394 // Remove HowTo step
395 $(document).on('click', '.remove-howto-step', function (e) {
396 e.preventDefault();
397 const container = $(this).closest('.howto-steps-list');
398 $(this).closest('.howto-step-item').fadeOut(200, function () {
399 $(this).remove();
400 container.find('.howto-step-item').each(function (index) {
401 $(this).find('label').first().text('Step ' + (index + 1) + ' Instructions: ');
402 $(this).find('label').first().append('<span style="color: #dc3232;">*</span>');
403 });
404 });
405 });
406
407 // Add FAQ item
408 $(document).on('click', '.add-faq-item', function (e) {
409 e.preventDefault();
410 const container = $(this).siblings('.faq-items-list');
411 const count = container.find('.faq-item').length;
412 const schemaTypeItem = $(this).closest('.schema-type-item');
413 const schemaIndex = schemaTypeItem.attr('data-index');
414
415 const newItem = '<div class="faq-item" style="margin: 15px 0; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9;">' +
416 '<div class="schema-field">' +
417 '<label>Question ' + (count + 1) + ':</label>' +
418 '<input type="text" name="schema_markup[types][' + schemaIndex + '][fields][faq_items][' + count + '][question]" value="" placeholder="Enter your question" style="width: 100%; margin-bottom: 10px;">' +
419 '</div>' +
420 '<div class="schema-field">' +
421 '<label>Answer ' + (count + 1) + ':</label>' +
422 '<textarea name="schema_markup[types][' + schemaIndex + '][fields][faq_items][' + count + '][answer]" placeholder="Enter your answer" style="width: 100%; height: 80px; margin-bottom: 10px;"></textarea>' +
423 '</div>' +
424 '<button type="button" class="button remove-faq-item" style="background: #dc3232; color: white; border-color: #dc3232;">Remove Question</button>' +
425 '</div>';
426 container.append(newItem);
427 });
428
429 // Remove FAQ item
430 $(document).on('click', '.remove-faq-item', function (e) {
431 e.preventDefault();
432 const container = $(this).closest('.faq-items-list');
433 $(this).closest('.faq-item').fadeOut(200, function () {
434 $(this).remove();
435 // Update question numbers
436 container.find('.faq-item').each(function (index) {
437 $(this).find('label').first().text('Question ' + (index + 1) + ':');
438 $(this).find('label').last().text('Answer ' + (index + 1) + ':');
439 });
440 });
441 });
442
443 // Function to display validation errors
444 function displayValidationErrors(errors) {
445 let errorHtml = '<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; border-radius: 4px;">';
446 errorHtml += '<h3 style="margin: 0 0 15px 0; color: #856404; font-size: 16px;">';
447 errorHtml += '<span class="dashicons dashicons-warning" style="color: #ffc107; vertical-align: middle;"></span> ';
448 errorHtml += 'Validation Issues Detected';
449 errorHtml += '</h3>';
450 errorHtml += '<p style="margin: 0 0 10px 0; color: #856404;">Schema preview is not possible because the following issues are present:</p>';
451 errorHtml += '<ul style="margin: 0; padding-left: 20px; color: #856404;">';
452
453 // Group errors by schema type
454 const errorsByType = {};
455 errors.forEach(function (error) {
456 if (!errorsByType[error.schema_type]) {
457 errorsByType[error.schema_type] = [];
458 }
459 errorsByType[error.schema_type].push(error);
460 });
461
462 // Display errors grouped by schema type
463 for (const schemaType in errorsByType) {
464 errorHtml += '<li style="margin-bottom: 8px;"><strong>' + schemaType + ' Schema:</strong><ul style="margin-top: 5px;">';
465 errorsByType[schemaType].forEach(function (error) {
466 errorHtml += '<li>' + error.message + '</li>';
467 });
468 errorHtml += '</ul></li>';
469 }
470
471 errorHtml += '</ul>';
472 errorHtml += '</div>';
473
474 // Display in preview area
475 $('#schema-json-preview').html(errorHtml);
476 $('#schema-preview-output').slideDown();
477 $('#copy_schema_button').hide(); // Hide copy button for errors
478 }
479
480 // Preview Schema
481 $(document).on('click', '#preview_schema_button', function (e) {
482 e.preventDefault();
483
484 const button = $(this);
485 const originalText = button.html();
486 button.prop('disabled', true).html('<span class="dashicons dashicons-update dashicons-spin"></span> Generating Preview...');
487
488 // Collect form data
489 const formData = {
490 action: 'metasync_preview_schema',
491 nonce: metasyncSchemaMarkup.preview_schema_nonce,
492 post_id: $('#post_ID').val(),
493 schema_enabled: $('input[name="schema_markup[enabled]"]').is(':checked') ? 1 : 0,
494 schema_types: []
495 };
496
497 // Collect all schema types and their fields
498 $('.schema-type-item').each(function () {
499 const schemaTypeItem = $(this);
500 const schemaIndex = schemaTypeItem.attr('data-index');
501 const schemaType = schemaTypeItem.find('input[name*="[type]"]').val();
502
503 if (schemaType) {
504 const schemaTypeData = {
505 type: schemaType,
506 fields: {}
507 };
508
509 // Collect fields for this schema type
510 schemaTypeItem.find('[name*="schema_markup[types][' + schemaIndex + '][fields]"]').each(function () {
511 const name = $(this).attr('name');
512 const value = $(this).val();
513
514 // Check if it's an array field with empty brackets (e.g., ingredients[])
515 if (name.match(/\[\]$/)) {
516 // Extract field name before []
517 const matches = name.match(/schema_markup\[types\]\[(\d+)\]\[fields\]\[([^\]]+)\]\[\]/);
518 if (matches) {
519 const fieldName = matches[2];
520 if (!schemaTypeData.fields[fieldName]) {
521 schemaTypeData.fields[fieldName] = [];
522 }
523 schemaTypeData.fields[fieldName].push(value);
524 }
525 } else {
526 // Parse the field name to get the structure
527 const matches = name.match(/schema_markup\[types\]\[(\d+)\]\[fields\]\[([^\]]+)\](?:\[([^\]]+)\])?(?:\[([^\]]+)\])?/);
528 if (matches) {
529 const field1 = matches[2];
530 const field2 = matches[3];
531 const field3 = matches[4];
532
533 if (field3) {
534 // Nested array (e.g., faq_items[0][question])
535 if (!schemaTypeData.fields[field1]) {
536 schemaTypeData.fields[field1] = [];
537 }
538 if (!schemaTypeData.fields[field1][field2]) {
539 schemaTypeData.fields[field1][field2] = {};
540 }
541 schemaTypeData.fields[field1][field2][field3] = value;
542 } else if (field2) {
543 // Simple nested (e.g., ingredients[0])
544 if (!schemaTypeData.fields[field1]) {
545 schemaTypeData.fields[field1] = [];
546 }
547 schemaTypeData.fields[field1].push(value);
548 } else {
549 // Simple field
550 schemaTypeData.fields[field1] = value;
551 }
552 }
553 }
554 });
555
556 formData.schema_types.push(schemaTypeData);
557 }
558 });
559
560 $.ajax({
561 url: metasyncSchemaMarkup.ajaxurl,
562 type: 'POST',
563 data: formData,
564 success: function (response) {
565 if (response.success) {
566 // Show JSON preview
567 $('#schema-json-preview').text(response.data.json);
568 $('#schema-preview-output').slideDown();
569 $('#copy_schema_button').show();
570 } else {
571 // Check if validation errors exist
572 if (response.data && response.data.validation_errors) {
573 // Display validation errors
574 displayValidationErrors(response.data.validation_errors);
575 } else {
576 alert('Error: ' + (response.data.message || 'Could not generate preview'));
577 }
578 }
579 },
580 error: function () {
581 alert('Error generating preview. Please try again.');
582 },
583 complete: function () {
584 button.prop('disabled', false).html(originalText);
585 }
586 });
587 });
588
589 // Copy Schema to Clipboard
590 $(document).on('click', '#copy_schema_button', function (e) {
591 e.preventDefault();
592 const schemaText = $('#schema-json-preview').text();
593
594 // Create temporary textarea
595 const temp = $('<textarea>');
596 $('body').append(temp);
597 temp.val(schemaText).select();
598 document.execCommand('copy');
599 temp.remove();
600
601 // Visual feedback
602 const button = $(this);
603 const originalHtml = button.html();
604 button.html('<span class="dashicons dashicons-yes" style="color: #46b450;"></span> Copied!');
605 setTimeout(function () {
606 button.html(originalHtml);
607 }, 2000);
608 });
609
610 // Close Preview
611 $(document).on('click', '#close_preview', function (e) {
612 e.preventDefault();
613 $('#schema-preview-output').slideUp();
614 $('#copy_schema_button').hide();
615 });
616
617 // Scope the numeric-only guards to the schema meta box ONLY.
618 // These were previously delegated on `document`, so the "block every
619 // non-digit key" branch below fired for keystrokes anywhere on the edit
620 // screen — silently swallowing letters in unrelated fields such as page
621 // builder heading inputs. Delegating from the schema container keeps the
622 // filter on `.recipe-time-input` fields (which only exist inside it).
623 var $schemaContainer = $('.metasync-schema-markup-container');
624
625 // Restrict recipe time inputs to numbers and decimals only
626 $schemaContainer.on('keydown', '.recipe-time-input', function (e) {
627 const key = e.key;
628 const value = $(this).val();
629
630 // Allow navigation and control keys
631 const allowedKeys = [
632 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
633 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
634 'Home', 'End'
635 ];
636
637 // Allow Ctrl/Cmd combinations (for copy, paste, select all, etc.)
638 if (e.ctrlKey || e.metaKey) {
639 return true;
640 }
641
642 // Allow navigation keys
643 if (allowedKeys.indexOf(key) !== -1) {
644 return true;
645 }
646
647 // Allow numbers (0-9)
648 if (key >= '0' && key <= '9') {
649 return true;
650 }
651
652 // Allow decimal point (period only), but only one
653 if (key === '.') {
654 // Prevent if decimal point already exists
655 if (value.indexOf('.') !== -1) {
656 e.preventDefault();
657 return false;
658 }
659 return true;
660 }
661
662 // Block all other keys
663 e.preventDefault();
664 return false;
665 });
666
667 // Prevent paste of non-numeric content
668 $schemaContainer.on('paste', '.recipe-time-input', function (e) {
669 e.preventDefault();
670
671 let pastedData = (e.originalEvent.clipboardData || window.clipboardData).getData('text');
672
673 // Normalize comma to period for decimal separator
674 pastedData = pastedData.replace(',', '.');
675
676 // Remove any non-numeric characters except decimal point
677 pastedData = pastedData.replace(/[^0-9.]/g, '');
678
679 // Ensure only one decimal point
680 const parts = pastedData.split('.');
681 if (parts.length > 2) {
682 pastedData = parts[0] + '.' + parts.slice(1).join('');
683 }
684
685 // Only allow numeric values with optional single decimal point
686 const numericRegex = /^[0-9]*\.?[0-9]*$/;
687
688 if (!numericRegex.test(pastedData)) {
689 return false;
690 }
691
692 // Insert the cleaned pasted value
693 const currentValue = $(this).val();
694 const cursorPosition = this.selectionStart;
695 const newValue = currentValue.substring(0, cursorPosition) + pastedData + currentValue.substring(this.selectionEnd);
696
697 // Final check: ensure no more than one decimal point in final value
698 if ((newValue.match(/\./g) || []).length > 1) {
699 return false;
700 }
701
702 $(this).val(newValue);
703 this.setSelectionRange(cursorPosition + pastedData.length, cursorPosition + pastedData.length);
704
705 return false;
706 });
707
708 // Collapsible override fields section toggle
709 $(document).on('click', '.schema-override-header', function (e) {
710 e.preventDefault();
711 const targetId = $(this).data('toggle-target');
712 const content = $('#' + targetId);
713 const icon = $(this).find('.toggle-icon');
714
715 if (content.is(':visible')) {
716 content.slideUp(200);
717 icon.css('transform', 'rotate(0deg)');
718 } else {
719 content.slideDown(200);
720 icon.css('transform', 'rotate(180deg)');
721 }
722 });
723
724 // Reset override field button
725 $(document).on('click', '.reset-override-field', function (e) {
726 e.preventDefault();
727 const fieldId = $(this).data('field');
728 const field = $('#' + fieldId);
729
730 // Determine placeholder based on field type
731 let defaultValue = '';
732 if (fieldId.indexOf('override_title_') !== -1) {
733 defaultValue = '{{post_title}}';
734 } else if (fieldId.indexOf('override_description_') !== -1) {
735 defaultValue = '{{post_description}}';
736 } else if (fieldId.indexOf('override_image_') !== -1) {
737 defaultValue = '{{featured_image}}';
738 } else {
739 defaultValue = field.data('default-value') || '';
740 }
741
742 // Reset the field value to placeholder
743 field.val(defaultValue);
744
745 // If it's an image field, hide the preview when resetting to placeholder
746 if (fieldId.indexOf('override_image_') !== -1) {
747 const index = fieldId.replace('override_image_', '');
748 const preview = $('#override_image_preview_' + index);
749 // If resetting to placeholder, hide preview (will use default featured image)
750 if (defaultValue === '{{featured_image}}') {
751 preview.hide();
752 }
753 }
754 });
755
756 // Image uploader for override image field
757 let overrideImageUploader;
758 $(document).on('click', '.upload-image-button', function (e) {
759 e.preventDefault();
760 const targetIndex = $(this).data('target');
761 const fieldId = $(this).data('field-id');
762
763 // If the media frame already exists, reopen it
764 if (overrideImageUploader) {
765 overrideImageUploader.open();
766 overrideImageUploader.targetField = fieldId;
767 overrideImageUploader.targetPreview = 'override_image_preview_' + targetIndex;
768 return;
769 }
770
771 // Create the media frame
772 overrideImageUploader = wp.media({
773 title: 'Choose Schema Image',
774 button: {
775 text: 'Use this image'
776 },
777 multiple: false,
778 library: {
779 type: 'image'
780 }
781 });
782
783 // Store target field info
784 overrideImageUploader.targetField = fieldId;
785 overrideImageUploader.targetPreview = 'override_image_preview_' + targetIndex;
786
787 // When an image is selected, run a callback
788 overrideImageUploader.on('select', function () {
789 const attachment = overrideImageUploader.state().get('selection').first().toJSON();
790 $('#' + overrideImageUploader.targetField).val(attachment.url);
791 $('#' + overrideImageUploader.targetPreview + ' img').attr('src', attachment.url);
792 $('#' + overrideImageUploader.targetPreview).show();
793 });
794
795 // Open the uploader dialog
796 overrideImageUploader.open();
797 });
798
799 });
800
801
802