/**
|
* JVBase SEO Admin Interface
|
*
|
* Handles:
|
* - Tab navigation
|
* - Dynamic schema field loading
|
* - Save/reset functionality
|
* - Repeater field management
|
*/
|
(function($) {
|
'use strict';
|
|
const SEOAdmin = {
|
config: window.jvbSeoConfig || {},
|
|
init() {
|
this.bindTabs();
|
this.bindSchemaTypeChange();
|
this.bindSaveButtons();
|
this.bindResetButtons();
|
this.bindRepeaterFields();
|
this.bindImageSelectors();
|
},
|
|
/**
|
* Tab navigation
|
*/
|
bindTabs() {
|
$('.jvb-seo-tabs .tab-btn').on('click', function() {
|
const tab = $(this).data('tab');
|
|
// Update active tab button
|
$('.jvb-seo-tabs .tab-btn').removeClass('active');
|
$(this).addClass('active');
|
|
// Show corresponding content
|
$('.tab-content').removeClass('active');
|
$(`.tab-content[data-tab="${tab}"]`).addClass('active');
|
});
|
},
|
|
/**
|
* Dynamic schema field loading when type changes
|
*/
|
bindSchemaTypeChange() {
|
$(document).on('change', '.schema-type-select', async function() {
|
const $select = $(this);
|
const type = $select.val();
|
const $fieldset = $select.closest('.jvb-seo-fieldset');
|
const $schemaFields = $fieldset.find('.schema-fields');
|
|
if (!type) {
|
$schemaFields.hide().empty();
|
return;
|
}
|
|
// Fetch fields for this schema type
|
try {
|
const response = await fetch(
|
`${SEOAdmin.config.restUrl}schema-fields/${type}`,
|
{
|
headers: {
|
'X-WP-Nonce': SEOAdmin.config.nonce
|
}
|
}
|
);
|
|
const data = await response.json();
|
|
if (data.fields) {
|
SEOAdmin.renderSchemaFields($schemaFields, data.fields);
|
$schemaFields.show();
|
}
|
} catch (error) {
|
console.error('Failed to load schema fields:', error);
|
}
|
});
|
},
|
|
/**
|
* Render schema field mappings
|
*/
|
renderSchemaFields($container, fields) {
|
$container.empty();
|
|
Object.entries(fields).forEach(([key, config]) => {
|
const $field = $(`
|
<div class="form-field schema-field-mapping" data-field="${key}">
|
<label>${config.label || key}</label>
|
<input type="text" name="schema_fields[${key}]"
|
value="" class="regular-text template-field"
|
placeholder="{{field_name}}">
|
${config.description ? `<span class="description">${config.description}</span>` : ''}
|
</div>
|
`);
|
$container.append($field);
|
});
|
},
|
|
/**
|
* Save button handlers
|
*/
|
bindSaveButtons() {
|
// Save site config
|
$('#save-site-config').on('click', async function() {
|
const $btn = $(this);
|
$btn.prop('disabled', true).text('Saving...');
|
|
const data = SEOAdmin.collectSiteFormData();
|
|
try {
|
const response = await fetch(`${SEOAdmin.config.restUrl}site`, {
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': SEOAdmin.config.nonce
|
},
|
body: JSON.stringify(data)
|
});
|
|
const result = await response.json();
|
|
if (result.success) {
|
SEOAdmin.showNotice('Settings saved successfully.', 'success');
|
} else {
|
SEOAdmin.showNotice(result.message || 'Failed to save.', 'error');
|
}
|
} catch (error) {
|
SEOAdmin.showNotice('Network error. Please try again.', 'error');
|
} finally {
|
$btn.prop('disabled', false).text('Save Site Settings');
|
}
|
});
|
|
// Save content type config
|
$(document).on('click', '.save-content-type', async function() {
|
const $btn = $(this);
|
const $container = $btn.closest('.jvb-seo-content-type');
|
const type = $container.data('type');
|
const objectType = $container.data('object-type');
|
|
$btn.prop('disabled', true).text('Saving...');
|
|
const data = SEOAdmin.collectContentTypeFormData($container);
|
|
try {
|
const response = await fetch(
|
`${SEOAdmin.config.restUrl}content/${objectType}/${type}`,
|
{
|
method: 'POST',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': SEOAdmin.config.nonce
|
},
|
body: JSON.stringify(data)
|
}
|
);
|
|
const result = await response.json();
|
|
if (result.success) {
|
SEOAdmin.showNotice('Settings saved.', 'success');
|
} else {
|
SEOAdmin.showNotice(result.message || 'Failed to save.', 'error');
|
}
|
} catch (error) {
|
SEOAdmin.showNotice('Network error.', 'error');
|
} finally {
|
$btn.prop('disabled', false).text('Save');
|
}
|
});
|
},
|
|
/**
|
* Reset button handlers
|
*/
|
bindResetButtons() {
|
$(document).on('click', '.reset-to-defaults', async function() {
|
if (!confirm('Reset to default settings? This cannot be undone.')) {
|
return;
|
}
|
|
const $btn = $(this);
|
const $container = $btn.closest('.jvb-seo-content-type');
|
const type = $container.data('type');
|
const objectType = $container.data('object-type');
|
|
$btn.prop('disabled', true).text('Resetting...');
|
|
try {
|
const response = await fetch(
|
`${SEOAdmin.config.restUrl}content/${objectType}/${type}/reset`,
|
{
|
method: 'POST',
|
headers: {
|
'X-WP-Nonce': SEOAdmin.config.nonce
|
}
|
}
|
);
|
|
const result = await response.json();
|
|
if (result.success) {
|
// Reload page to show defaults
|
location.reload();
|
} else {
|
SEOAdmin.showNotice(result.message || 'Failed to reset.', 'error');
|
}
|
} catch (error) {
|
SEOAdmin.showNotice('Network error.', 'error');
|
} finally {
|
$btn.prop('disabled', false).text('Reset to Defaults');
|
}
|
});
|
},
|
|
/**
|
* Repeater field management
|
*/
|
bindRepeaterFields() {
|
// Add row
|
$(document).on('click', '.repeater-field .add-row', function() {
|
const $repeater = $(this).closest('.repeater-field');
|
const fieldName = $repeater.data('field');
|
|
const $row = $(`
|
<div class="repeater-row">
|
<input type="url" name="${fieldName}[]" value="" class="regular-text">
|
<button type="button" class="button remove-row">×</button>
|
</div>
|
`);
|
|
$repeater.find('.add-row').before($row);
|
});
|
|
// Remove row
|
$(document).on('click', '.repeater-field .remove-row', function() {
|
$(this).closest('.repeater-row').remove();
|
});
|
},
|
|
/**
|
* WordPress Media Library image selector
|
*/
|
bindImageSelectors() {
|
$(document).on('click', '.select-image', function(e) {
|
e.preventDefault();
|
|
const $button = $(this);
|
const $field = $button.siblings('input[type="hidden"]');
|
const $preview = $button.siblings('.image-preview');
|
|
const frame = wp.media({
|
title: 'Select Image',
|
button: { text: 'Use Image' },
|
multiple: false
|
});
|
|
frame.on('select', function() {
|
const attachment = frame.state().get('selection').first().toJSON();
|
$field.val(attachment.id);
|
$preview.html(`<img src="${attachment.sizes?.thumbnail?.url || attachment.url}" style="max-height:50px;">`);
|
});
|
|
frame.open();
|
});
|
},
|
|
/**
|
* Collect site form data
|
*/
|
collectSiteFormData() {
|
const $form = $('[data-tab="site"]');
|
const data = {};
|
|
// Single fields
|
$form.find('input[name], textarea[name], select[name]').each(function() {
|
const name = $(this).attr('name');
|
if (!name.endsWith('[]')) {
|
data[name] = $(this).val();
|
}
|
});
|
|
// Repeater fields (sameAs)
|
$form.find('[name="organization_sameas[]"]').each(function() {
|
if (!data.organization_sameas) {
|
data.organization_sameas = [];
|
}
|
const val = $(this).val();
|
if (val) {
|
data.organization_sameas.push(val);
|
}
|
});
|
|
return data;
|
},
|
|
/**
|
* Collect content type form data
|
*/
|
collectContentTypeFormData($container) {
|
const data = {
|
meta_title: $container.find('[name="meta_title"]').val(),
|
meta_description: $container.find('[name="meta_description"]').val(),
|
schema_type: $container.find('[name="schema_type"]').val(),
|
schema_fields: {}
|
};
|
|
// Collect schema field mappings
|
$container.find('[name^="schema_fields"]').each(function() {
|
const match = $(this).attr('name').match(/schema_fields\[(\w+)\]/);
|
if (match) {
|
data.schema_fields[match[1]] = $(this).val();
|
}
|
});
|
|
return data;
|
},
|
|
/**
|
* Show notice message
|
*/
|
showNotice(message, type = 'info') {
|
const $notice = $(`
|
<div class="notice notice-${type} is-dismissible">
|
<p>${message}</p>
|
</div>
|
`);
|
|
// Remove existing notices
|
$('.jvb-seo-admin .notice').remove();
|
|
// Add new notice
|
$('.jvb-seo-admin').prepend($notice);
|
|
// Auto-dismiss after 5 seconds
|
setTimeout(() => $notice.fadeOut(), 5000);
|
}
|
};
|
|
// Initialize when DOM is ready
|
$(document).ready(() => SEOAdmin.init());
|
|
})(jQuery);
|