Files
Jacek Pyziak cd264483f8 Add PSR HTTP Message Interfaces and Dependencies
- Implemented StreamInterface, UploadedFileInterface, and UriInterface as per PSR standards.
- Added getallheaders function to retrieve HTTP headers in a compatible manner.
- Included LICENSE files for ralouphie/getallheaders and symfony/deprecation-contracts.
- Introduced function for triggering deprecation notices in Symfony.
2025-12-28 12:44:00 +01:00

1449 lines
65 KiB
JavaScript

jQuery(document).ready(function($) {
// Utility function to escape HTML entities for safe display
function escapeHtml(text) {
if (typeof text !== 'string') return text;
const map = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
// Utility: Reset modal to initial state
function resetImportModalUI() {
$('#atfpp-import-success-ui').addClass('atfpp-hidden');
$('#atfpp-import-glossary-ui').show();
$('#file-name-display').text('Select a CSV file to upload');
$('#importing-file-name').text('');
$('#atfpp-csv-upload').val('');
}
// Cache selectors used multiple times
let $glossaryTable = $('.atfpp-glossary-table');
const $languageFilters = $('.atfpp-language-filters');
const $alphabet = $('.atfpp-alphabet');
const $addGlossaryForm = $('#atfpp-add-glossary-form');
const $addBtn = $('.atfpp-add-btn');
const $importBtn = $('.atfpp-import-btn');
// Open modal
$addBtn.on('click', function() {
$('#atfpp-glossary-modal-add').removeClass('atfpp-hidden').addClass('active');
$('body').addClass('atfpp-modal-open');
// Reset add form and translations section
$addGlossaryForm.find('.atfpp-add-translations').removeClass('atfpp-show').css('display', 'none');
$addGlossaryForm.find('.atfpp-translation-field').show();
$addGlossaryForm[0].reset();
});
$importBtn.on('click', function() {
resetImportModalUI();
$('#atfpp-glossary-modal-import').removeClass('atfpp-hidden').addClass('active');
});
// Close modal & reset
$(document).on('click', '.atfpp-modal-close-btn, .atfpp-glossary-modal-actions-left', function() {
const modal = $(this).closest('.atfpp-glossary-modal');
const importSuccessUI = modal.find('#atfpp-import-success-ui');
if ((importSuccessUI.length && !importSuccessUI.hasClass('atfpp-hidden') && importSuccessUI.is(':visible'))) {
window.location.reload();
}
modal.addClass('atfpp-hidden').removeClass('active');
modal.find('form').show();
modal.find('#add-glossary-success').addClass('atfpp-hidden');
$('body').removeClass('atfpp-modal-open');
resetImportModalUI();
});
// File input change
$('#atfpp-csv-upload').on('change', function(e) {
const file = e.target.files[0];
if (!file || !file.name.toLowerCase().endsWith('.csv')) {
alert('Please upload a valid CSV file.');
$(this).val('');
return;
}
const reader = new FileReader();
reader.onload = function(event) {
const text = event.target.result;
const lines = text.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim());
const data = lines.slice(1).map(line => {
const values = line.split(',').map(v => v.trim());
const obj = {};
headers.forEach((header, i) => {
obj[header] = values[i] || '';
});
return obj;
});
};
reader.readAsText(file);
// If valid file:
$('#file-name-display').text(file.name);
$('#importing-file-name').text(file.name);
// Automatically import as soon as file is selected
const formData = new FormData();
formData.append('action', 'atfpp_import_glossary');
formData.append('csv_file', file);
formData.append('overwrite', false);
formData.append('_wpnonce', atfpp_glossary.nonce);
if (!$('.atfpp-glossary-loader').length) {
$('.atfpp-glossary-modal-content').append('<div class="atfpp-glossary-loader"><div class="atfpp-glossary-loader-spinner"></div></div>');
}
$.ajax({
url: atfpp_glossary.ajaxurl,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(resp) {
if (resp.success) {
// Remove loader
$('.atfpp-glossary-loader').remove();
$('#atfpp-import-glossary-ui').hide();
$('#atfpp-import-success-ui').removeClass('atfpp-hidden');
} else {
alert('Import failed: ' + (resp.data || 'Unknown error'));
}
},
error: function(jqXHR, textStatus, errorThrown) {
alert('An error occurred while importing the glossary. Please try again.');
}
});
});
// Modify the download link click handler
$('.atfpp-download-link').off('click').on('click', function(e) {
e.preventDefault();
// CSV content matching your image
const csvContent = [
[
'original_language_code',
'target_language_code',
'original_term',
'translated_term',
'description',
'kind'
],
['en', 'it', 'Page', 'Pagina', 'the page of a browser', 'general'],
['en', 'it', 'page', 'pagina', 'the page of a browser', 'general'],
['en', 'it', 'OnTheGoSystems', 'OnTheGoSystems', 'the name of my company', 'name']
].map(row => row.join(",")).join("\n");
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'sample-glossary.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
$(document).off('click', '.atfpp-edit-btn, .atfpp-edit-btn-svg').on('click', '.atfpp-edit-btn, .atfpp-edit-btn-svg', function(e) {
e.preventDefault();
e.stopPropagation();
// Handle any existing edit rows first
const $existingEditRow = $('.atfpp-glossary-edit-row');
if ($existingEditRow.length) {
// Show the original row that was being edited
$existingEditRow.prev('tr').show();
// Remove the edit row
$existingEditRow.remove();
}
// Find the row being edited
const $row = $(this).closest('tr');
const term = $row.data('term') || '';
const desc = $row.find('.atfpp-entry-desc').text().trim() || '';
const type = $row.data('type') || 'general';
const source_lang = $row.data('original-language') || '';
// Validate required data
if (!source_lang) {
console.warn('Missing source language for row');
return;
}
// Safely build translations object
const translations = {};
if (typeof atfpp_glossary !== 'undefined' && Array.isArray(atfpp_glossary.atfpp_languages)) {
atfpp_glossary.atfpp_languages.forEach(function(lang) {
if (!lang || !lang.code || lang.code === source_lang) return;
const $cell = $row.find(`td.atfpp-lang-col-${lang.code}`);
const $term = $cell.find('.atfpp-translated-term');
let value = '';
if ($term.length) {
// Try to get full text from data attribute first
value = $term.data('full-text') || $term.attr('data-full-text') || $term.text().trim();
}
translations[lang.code] = typeof value === 'string' ? value : (value ? String(value) : '');
});
}
const safeTerm = typeof term === 'string' ? term : '';
const safeDesc = typeof desc === 'string' ? desc : '';
const safeType = typeof type === 'string' ? type : 'general';
const safeSourceLang = typeof source_lang === 'string' ? source_lang : '';
try {
const $templateScript = $('#atfpp-glossary-edit-row-template');
const templateHtml = $templateScript.length ? $templateScript.html() : '';
if (!templateHtml) {
console.error('Template not found or empty');
alert('Edit template not found. Please refresh the page.');
return;
}
const templateFn = _.template(templateHtml);
const editRowHtml = templateFn({
term: safeTerm,
desc: safeDesc,
type: safeType,
translations: translations,
languages: atfpp_glossary.atfpp_languages.filter(lang => lang.code !== safeSourceLang),
source_lang: safeSourceLang
});
$row.after(editRowHtml);
$row.hide();
$row.next('.atfpp-glossary-edit-row').show();
} catch (error) {
console.error('Error generating edit row:', error, {
safeTerm,
safeDesc,
safeType,
translations,
safeSourceLang
});
alert('An error occurred while trying to edit this entry. Please try again.');
}
});
// Add input event handler for real-time validation
$glossaryTable.on('input', '.atfpp-edit-translation, .atfpp-edit-term, .atfpp-edit-desc', function() {
const $textarea = $(this);
const $error = $textarea.next('.atfpp-translation-error');
const value = $textarea.val().trim();
const maxLength = $textarea.hasClass('atfpp-edit-term') || $textarea.hasClass('atfpp-edit-desc') ? 240 : 220;
if (value.length > maxLength) {
$textarea.addClass('error');
$error.text(`Must be less than ${maxLength} characters`).show();
} else {
$textarea.removeClass('error');
$error.hide();
}
});
// Update the save handler to include validation
$('.atfpp-glossary-table-wrapper').off('click', '.atfpp-save-edit-btn').on('click', '.atfpp-save-edit-btn', function(e) {
e.preventDefault();
const $editRow = $(this).closest('tr');
const $origRow = $editRow.prev('tr');
// Validate required fields
const $termField = $editRow.find('.atfpp-edit-term');
const $descField = $editRow.find('.atfpp-edit-desc');
const term = $termField.val().trim();
const desc = $descField.val().trim();
const type = $editRow.find('.atfpp-edit-type').val();
const source_lang = $origRow.data('original-language');
let hasError = false;
// Add validation for term and description length
if (term.length > 240) {
$termField.addClass('error');
$termField.next('.atfpp-translation-error').text('Term must be less than 240 characters.').show();
hasError = true;
}
if (desc.length > 240) {
$descField.addClass('error');
$descField.next('.atfpp-translation-error').text('Description must be less than 240 characters.').show();
hasError = true;
}
// Check translation lengths
$editRow.find('.atfpp-edit-translation').each(function() {
const $textarea = $(this);
const value = $textarea.val().trim();
if (value.length > 240) {
$textarea.addClass('error');
$textarea.next('.atfpp-translation-error').text('Translation must be less than 240 characters.').show();
hasError = true;
}
});
if (hasError) {
return;
}
const translations = {};
$editRow.find('.atfpp-edit-translation').each(function() {
const lang = $(this).data('lang');
if (lang) {
const value = $(this).val().trim();
// Only include non-empty translations
if (value !== '') {
translations[lang] = value;
}
}
});
// Modified AJAX request
$.ajax({
url: atfpp_glossary.ajaxurl,
type: 'POST',
data: {
action: 'atfpp_update_glossary',
_wpnonce: atfpp_glossary.nonce,
data: {
term: term,
description: desc,
type: type,
source_lang: source_lang,
translations: translations,
original_term: $origRow.data('term'),
original_type: $origRow.data('type'),
original_source_lang: source_lang
}
},
success: function(resp) {
if (resp.success && resp.data && resp.data.updated_entry) {
const updatedEntry = resp.data.updated_entry;
// Update the original row with data from server response
$origRow.data('term', updatedEntry.original_term);
$origRow.data('type', updatedEntry.kind);
// Update term and description from server data
$origRow.find('.atfpp-entry-title').text(updatedEntry.original_term);
$origRow.find('.atfpp-entry-desc').text(updatedEntry.description || '');
// Update type badge from server data
$origRow.find('.atfpp-type-badge')
.attr('class', 'atfpp-type-badge ' + updatedEntry.kind)
.text(updatedEntry.kind.charAt(0).toUpperCase() + updatedEntry.kind.slice(1));
// Update translations from server data
if (updatedEntry.translations && Array.isArray(updatedEntry.translations)) {
// Create a map of translations from server - only include non-empty translations
const serverTranslations = {};
updatedEntry.translations.forEach(function(translation) {
if (translation.target_language_code &&
translation.translated_term &&
translation.translated_term.trim() !== '') {
serverTranslations[translation.target_language_code] = translation.translated_term.trim();
}
});
// Update all language columns based on server data
$origRow.find('[class*="atfpp-lang-col-"]').each(function() {
const $cell = $(this);
const langCode = $cell.attr('class').match(/atfpp-lang-col-([a-zA-Z_-]+)/);
if (langCode && langCode[1] && !$cell.data('is-source')) {
const lang = langCode[1];
const translatedTerm = serverTranslations[lang];
if (translatedTerm && translatedTerm.trim() !== '') {
// Has translation - show it
const truncatedText = translatedTerm.length > 7 ? translatedTerm.substring(0, 7) + '…' : translatedTerm;
const safeTranslation = escapeHtml(translatedTerm);
const safeTruncated = escapeHtml(truncatedText);
$cell.html(`
<span class="atfpp-translated-term"
data-full-text="${safeTranslation}"
title="${safeTranslation}">
${safeTruncated}
</span>
`);
} else {
// No translation - show add button
$cell.html(`
<button type="button" class="atfpp-edit-btn-svg" data-term="${escapeHtml(updatedEntry.original_term)}" data-source-lang="${escapeHtml(updatedEntry.original_language_code)}">
<img src="${atfpp_glossary.url}admin/atfpp-dashboard/images/file.svg" alt="Add Translation" />
</button>
`);
}
}
});
}
// Show the updated row and remove edit row
$origRow.show();
$editRow.remove();
// Reapply filters and update UI
filterGlossaryRows();
updateGlossaryTableVisibility();
updateAlphabetButtonStates();
applyZebraStriping();
} else {
alert('Update failed: ' + (resp.data?.message || resp.data || 'Unknown error'));
}
},
error: function(xhr, status, error) {
console.error('AJAX Error:', status, error);
alert('An error occurred while saving. Please try again.');
}
});
});
function applyZebraStriping() {
$('.atfpp-glossary-table tbody tr').removeClass('atfpp-row-striped');
$('.atfpp-glossary-table tbody tr:visible:not(.atfpp-glossary-edit-row)').each(function(i) {
if (i % 2 === 1) {
$(this).addClass('atfpp-row-striped');
}
});
}
function updateGlossaryTableVisibility() {
var $tableWrapper = $('.atfpp-glossary-table-wrapper');
var $table = $tableWrapper.find('.atfpp-glossary-table');
var $visibleRows = $table.find('tbody tr:visible');
if ($visibleRows.length === 0) {
$tableWrapper.hide();
if ($('#atfpp-no-results').length === 0) {
$tableWrapper.after('<div id="atfpp-no-results" style="text-align:center; margin: 32px 0; color: #888; font-size: 1.2em;">No glossary entries found.</div>');
}
} else {
$tableWrapper.show();
$('#atfpp-no-results').remove();
}
applyZebraStriping();
}
function updateAlphabetButtonStates() {
$alphabet.find('.atfpp-alphabet-btn').prop('disabled', false);
var visibleLetters = {};
$glossaryTable.find('tbody tr:visible').each(function() {
var letter = $(this).data('letter');
if (letter) visibleLetters[letter] = true;
});
$alphabet.find('.atfpp-alphabet-btn').each(function() {
var $btn = $(this);
var letter = $btn.data('letter');
if (!visibleLetters[letter]) {
$btn.prop('disabled', true);
$btn.removeClass('active');
}
});
// After updating the table and language filter buttons
var $alphabetBtns = $('.atfpp-alphabet-btn');
var $visibleRows = $glossaryTable.find('tbody tr:visible');
// If there are no visible rows, remove active state from all alphabet buttons
if ($visibleRows.length === 0) {
$alphabetBtns.removeClass('active');
}
}
// Language Filter Buttons
$(document).off('click', '.atfpp-lang-filter-btn').on('click', '.atfpp-lang-filter-btn', function() {
$('.atfpp-glossary-table-wrapper').show();
$('#atfpp-no-results').remove();
// Reset alphabet filter
$('.atfpp-alphabet-btn').removeClass('active');
var $btn = $(this);
var selectedLang = $btn.data('lang');
var $table = $('.atfpp-glossary-table');
var defaultLang = $table.data('default-lang');
var previousSelectedLang = $('.atfpp-lang-filter-btn.active').data('lang');
// Update active state
$('.atfpp-lang-filter-btn').removeClass('active');
$btn.addClass('active');
// First show all columns
$table.find('th[data-lang], td[data-lang]').show();
// Show the previously hidden default language column
if (defaultLang) {
$table.find(`th[data-lang="${defaultLang}"], td[data-lang="${defaultLang}"]`).show();
}
// Hide the newly selected language column when it's the source
if (selectedLang) {
$table.find(`td[data-lang="${selectedLang}"][data-is-source="true"]`).hide();
$table.find(`th[data-lang="${selectedLang}"]`).hide();
}
// Apply filters to rows
$('.atfpp-glossary-table tbody tr').each(function() {
var $row = $(this);
var rowOriginalLang = $row.data('original-language');
var rowType = $row.data('type');
var show = true;
// Show row if it matches the selected language
if (selectedLang) {
show = (rowOriginalLang === selectedLang);
if (show) {
// For visible rows, ensure correct column visibility
$row.find('td[data-lang]').each(function() {
var $cell = $(this);
var cellLang = $cell.data('lang');
var isSource = $cell.data('is-source') === true;
// Hide if this is the source language column
if (cellLang === selectedLang && isSource) {
$cell.hide();
} else {
$cell.show();
}
});
}
}
// Apply type filter if active
var currentType = $('.atfpp-glossary-type').val();
if (show && currentType) {
show = (rowType === currentType);
}
// Handle edit rows
if ($row.hasClass('atfpp-glossary-edit-row')) {
show = $row.prev('tr').is(':visible');
}
$row.toggle(show);
});
updateGlossaryTableVisibility();
updateAlphabetButtonStates();
applyZebraStriping();
});
// Update filterGlossaryRows function
function filterGlossaryRows() {
var selectedLang = $('.atfpp-lang-filter-btn.active').data('lang') || '';
var selectedType = $('.atfpp-glossary-type').val() || '';
var selectedLetter = $('.atfpp-alphabet-btn.active:not([disabled])').data('letter') || '';
var search = $('.atfpp-search').val().toLowerCase();
$('.atfpp-glossary-table tbody tr').each(function() {
var $row = $(this);
var rowOriginalLang = $row.data('original-language');
var rowType = $row.data('type');
var rowLetter = $row.data('letter');
var term = $row.find('.atfpp-entry-title').text().toLowerCase();
var desc = $row.find('.atfpp-entry-desc').text().toLowerCase();
// Start with strict language filter
var show = (!selectedLang || rowOriginalLang === selectedLang);
// Apply other filters only if row passes language filter
if (show && selectedType) {
show = (rowType === selectedType);
}
if (show && selectedLetter) {
show = (rowLetter === selectedLetter);
}
if (show && search) {
show = (term.indexOf(search) !== -1 || desc.indexOf(search) !== -1);
}
// Handle edit rows visibility
if ($row.hasClass('atfpp-glossary-edit-row')) {
show = $row.prev('tr').is(':visible');
}
$row.toggle(show);
});
updateGlossaryTableVisibility();
applyZebraStriping();
}
// On page load, if filter buttons exist, trigger click on the first one
if ($languageFilters.length && $languageFilters.find('.atfpp-lang-filter-btn').length > 0) {
$languageFilters.find('.atfpp-lang-filter-btn').first().trigger('click');
}
// Alphabet filter functionality
$(document).off('click', '.atfpp-alphabet-btn:not([disabled])').on('click', '.atfpp-alphabet-btn:not([disabled])', function() {
var $btn = $(this);
if ($btn.hasClass('active')) {
$btn.removeClass('active');
} else {
$(document).find('.atfpp-alphabet-btn').removeClass('active');
$btn.addClass('active');
}
filterGlossaryRows();
applyZebraStriping();
});
// Handle Delete in glossary table
$(document).off('click', '.atfpp-delete-btn').on('click', '.atfpp-delete-btn', function(e) {
e.preventDefault();
e.stopPropagation();
if (!confirm('Are you sure you want to delete this glossary entry?')) return;
const $deleteBtn = $(this);
const $row = $deleteBtn.closest('tr');
const term = $row.data('term');
const source_lang = $row.data('original-language');
// Store original button content
const originalContent = $deleteBtn.html();
// Show loading state only on the delete button
$deleteBtn.addClass('atfpp-delete-loading');
$deleteBtn.html('<div class="atfpp-delete-spinner"></div>');
$deleteBtn.prop('disabled', true);
$.post(atfpp_glossary.ajaxurl, {
action: 'atfpp_delete_glossary',
_wpnonce: atfpp_glossary.nonce,
term: term,
source_lang: source_lang
}, function(resp) {
if (resp.success) {
$row.fadeOut(300, function() {
$(this).remove();
// After row removal, check if any rows are visible
var $visibleRows = $glossaryTable.find('tbody tr:visible');
if ($visibleRows.length === 0) {
var $langBtns = $('.atfpp-lang-filter-btn');
var $activeBtn = $langBtns.filter('.active');
var idx = $langBtns.index($activeBtn);
// Remove the filter button for the language with no data
$activeBtn.remove();
$langBtns = $('.atfpp-lang-filter-btn'); // Refresh after removal
// Disable and deactivate all alphabet buttons if no data
var $alphabetBtns = $('.atfpp-alphabet-btn');
$alphabetBtns.removeClass('active').prop('disabled', true);
// Hide language filter if only one button remains
if ($langBtns.length <= 1) {
$('.atfpp-language-filters').hide();
}
// After removal, $langBtns is refreshed
if ($langBtns.length > 0) {
// Try to activate the button at the same index (which is now the next one)
var newIdx = Math.min(idx, $langBtns.length - 1);
$langBtns.eq(newIdx).trigger('click');
} else {
updateGlossaryTableVisibility();
}
} else {
updateGlossaryTableVisibility();
}
});
} else {
// Reset button state on error
$deleteBtn.removeClass('atfpp-delete-loading');
$deleteBtn.html(originalContent);
$deleteBtn.prop('disabled', false);
alert('Delete failed: ' + (resp.data || 'Unknown error'));
}
}).fail(function() {
// Reset button state on AJAX failure
$deleteBtn.removeClass('atfpp-delete-loading');
$deleteBtn.html(originalContent);
$deleteBtn.prop('disabled', false);
alert('An error occurred while deleting. Please try again.');
});
});
// Show/hide translations section based on required fields
function toggleTranslationsSection($form) {
var $translations = $form.find('.atfpp-add-translations');
var sourceLang = $form.find('.atfpp-add-source-lang').val() || '';
var term = $form.find('.atfpp-add-term').val() || '';
var type = $form.find('.atfpp-add-type').val() || '';
// Remove hidden class from all fields first
$translations.find('.atfpp-translation-field').removeClass('atfpp-hidden');
// Hide the translation field for the selected original language
if (sourceLang) {
$translations.find('.atfpp-translation-field-' + sourceLang).addClass('atfpp-hidden');
}
if (
term.trim() !== '' &&
type.trim() !== '' &&
sourceLang.trim() !== ''
) {
$translations.addClass('atfpp-show').css('display', 'flex');
} else {
$translations.removeClass('atfpp-show').css('display', 'none');
}
}
// Attach event listeners to required fields
$addGlossaryForm.on('input', '.atfpp-add-term, .atfpp-add-type, .atfpp-add-source-lang', function() {
toggleTranslationsSection($addGlossaryForm);
});
// Ensure correct state on page load
$(function() {
toggleTranslationsSection($addGlossaryForm);
});
// Add input event handler for translation fields in add form
$addGlossaryForm.on('input', '.atfpp-add-translation', function() {
const $textarea = $(this);
const $error = $textarea.next('.atfpp-translation-error');
const value = $textarea.val().trim();
const maxLength = 240;
if (value.length > maxLength) {
$textarea.addClass('error');
$error.text(`Must be less than ${maxLength} characters`).show();
} else {
$textarea.removeClass('error');
$error.hide();
}
});
// Add this helper function at the top of the file
function sanitizeInput(input) {
return input.replace(/[<>]/g, ''); // Remove < and > characters
}
// Update the add glossary form submission handler
$addGlossaryForm.off('submit').on('submit', function(e) {
e.preventDefault();
const $form = $(this);
const $termField = $form.find('.atfpp-add-term');
const $descField = $form.find('.atfpp-add-desc');
const term = sanitizeInput($termField.val().trim());
const desc = sanitizeInput($descField.val().trim());
let hasError = false;
// Check for script tags or other potentially harmful content
if ($termField.val().trim() !== term || $descField.val().trim() !== desc) {
alert('Invalid input');
hasError = true;
}
// Check term and description length
if (term.length > 240) {
$termField.addClass('error');
$termField.next('.atfpp-translation-error').text('Term must be less than 240 characters.').show();
hasError = true;
}
if (desc.length > 240) {
$descField.addClass('error');
$descField.next('.atfpp-translation-error').text('Description must be less than 240 characters.').show();
hasError = true;
}
// Check translation lengths and sanitize
$form.find('.atfpp-add-translation').each(function() {
const $textarea = $(this);
const originalValue = $textarea.val().trim();
const sanitizedValue = sanitizeInput(originalValue);
if (originalValue !== sanitizedValue) {
$textarea.addClass('error');
$textarea.next('.atfpp-translation-error').text('Invalid input').show();
hasError = true;
}
if (sanitizedValue.length > 240) {
$textarea.addClass('error');
$textarea.next('.atfpp-translation-error').text('Translation must be less than 240 characters.').show();
hasError = true;
}
});
if (hasError) {
return;
}
// Check for duplicate in the table
var duplicate = false;
$('.atfpp-glossary-table tbody tr').each(function() {
var rowTerm = $(this).data('term');
var rowLang = $(this).data('original-language');
if (rowTerm && rowLang && rowTerm.trim().toLowerCase() === term.toLowerCase() && rowLang === $form.find('.atfpp-add-source-lang').val()) {
duplicate = true;
return false; // break loop
}
});
if (duplicate) {
alert('This term already exists in this language.');
return;
}
if (!$('.atfpp-glossary-loader').length) {
const translations = $('#atfpp-add-glossary-form').find('.atfpp-add-translations')
translations.removeClass('atfpp-show').css('display', 'none');
$('.atfpp-glossary-modal-content').append('<div class="atfpp-glossary-loader"><div class="atfpp-glossary-loader-spinner"></div></div>');
}
const data = {
action: 'atfpp_add_glossary',
_wpnonce: atfpp_glossary.nonce,
type: $form.find('.atfpp-add-type').val(),
term: term,
description: desc,
source_lang: $form.find('.atfpp-add-source-lang').val(),
translations: {}
};
(atfpp_glossary.atfpp_languages || []).forEach(function(lang) {
const langCode = lang.code;
if (langCode === data.source_lang) return;
const translationValue = sanitizeInput($form.find('[name="translation_' + langCode + '"]').val().trim());
// Only include non-empty translations
if (translationValue !== '' && translationValue.trim() !== '') {
data.translations[langCode] = translationValue;
}
});
$.post(atfpp_glossary.ajaxurl, data, function(resp) {
$('.atfpp-glossary-loader').remove();
if (resp.success) {
// Check if we have the added entry data from server
if (resp.data && resp.data.added_entry) {
$form.hide();
$('#add-glossary-success').removeClass('atfpp-hidden');
// Use the data returned from the server
const addedEntry = resp.data.added_entry;
const savedTerm = addedEntry.original_term;
const savedDesc = addedEntry.description || '';
const savedType = addedEntry.kind;
const savedSourceLang = addedEntry.original_language_code;
// Create a map of saved translations - only include non-empty translations
const savedTranslations = {};
if (addedEntry.translations && Array.isArray(addedEntry.translations)) {
addedEntry.translations.forEach(function(translation) {
if (translation.target_language_code &&
translation.translated_term &&
translation.translated_term.trim() !== '') {
savedTranslations[translation.target_language_code] = translation.translated_term.trim();
}
});
}
// Determine the previous state before adding the new entry
const existingRows = $('.atfpp-glossary-table tbody tr');
const uniqueLanguagesBeforeAdd = new Set();
existingRows.each(function() {
const lang = $(this).data('original-language');
if (lang) uniqueLanguagesBeforeAdd.add(lang);
});
const wasSingleLanguageMode = uniqueLanguagesBeforeAdd.size <= 1;
// Determine if we're in single language mode after adding (like PHP logic)
const uniqueLanguagesAfterAdd = new Set(uniqueLanguagesBeforeAdd);
uniqueLanguagesAfterAdd.add(savedSourceLang);
const singleLanguageMode = uniqueLanguagesAfterAdd.size === 1;
const singleLanguageCode = singleLanguageMode ? savedSourceLang : '';
// Initialize translationsHtml
let translationsHtml = '';
// Generate columns for languages, respecting single language mode
atfpp_glossary.atfpp_languages.forEach(lang => {
const langCode = lang.code;
// Skip source language column if in single language mode
if (singleLanguageMode && langCode === singleLanguageCode) {
return;
}
if (langCode === savedSourceLang) {
// Source language - show the original term (only when not in single language mode)
const safeTerm = escapeHtml(savedTerm);
translationsHtml += `
<td colspan="2" class="atfpp-lang-col-${langCode}" data-lang="${langCode}" data-is-source="true">
<span class="atfpp-source-term">
${safeTerm}
</span>
</td>
`;
} else {
// Translation languages - check if we have a saved translation
const savedTranslation = savedTranslations[langCode];
if (!savedTranslation || savedTranslation.trim() === '') {
// No translation - show add button
translationsHtml += `
<td colspan="2" class="atfpp-lang-col-${langCode}" data-lang="${langCode}">
<button type="button" class="atfpp-edit-btn-svg" data-term="${escapeHtml(savedTerm)}" data-source-lang="${escapeHtml(savedSourceLang)}">
<img src="${atfpp_glossary.url}admin/atfpp-dashboard/images/file.svg" alt="Add Translation" />
</button>
</td>
`;
} else {
// Has translation - show it
const truncatedText = savedTranslation.length > 7 ? savedTranslation.substring(0, 7) + '…' : savedTranslation;
const safeTranslation = escapeHtml(savedTranslation);
const safeTruncated = escapeHtml(truncatedText);
translationsHtml += `
<td colspan="2" class="atfpp-lang-col-${langCode}" data-lang="${langCode}">
<span class="atfpp-translated-term"
data-full-text="${safeTranslation}"
title="${safeTranslation}">
${safeTruncated}
</span>
</td>
`;
}
}
});
const firstLetter = savedTerm ? (isNaN(savedTerm[0]) ? savedTerm[0].toUpperCase() : '123') : '';
const safeTerm = escapeHtml(savedTerm);
const safeDesc = escapeHtml(savedDesc);
const safeType = escapeHtml(savedType);
const safeSourceLang = escapeHtml(savedSourceLang);
const newEntryHtml = `
<tr data-term="${safeTerm}" data-original-language="${safeSourceLang}" data-type="${safeType}" data-letter="${firstLetter}">
<td>
<div class="atfpp-entry-title">${safeTerm}</div>
<div class="atfpp-entry-desc">${safeDesc}</div>
</td>
<td>
<span class="atfpp-type-badge ${safeType}">${safeType.charAt(0).toUpperCase() + safeType.slice(1)}</span>
</td>
${translationsHtml}
<td class="atfpp-actions-cell">
<div class="atfpp-action-buttons">
<button type="button" class="atfpp-edit-btn" data-term="${safeTerm}" data-source-lang="${safeSourceLang}">Edit</button>
<button type="button" class="atfpp-delete-btn" data-term="${safeTerm}" data-source-lang="${safeSourceLang}">Delete</button>
</div>
</td>
</tr>
`;
// Only recreate headers when transitioning from single to multi-language mode OR when no table exists
const needsHeaderUpdate = wasSingleLanguageMode && !singleLanguageMode;
// If the table does not exist, create it with headers
// OR if we're transitioning from single to multi-language mode, recreate headers
if ($('.atfpp-glossary-table').length === 0 || needsHeaderUpdate) {
// Save existing table body if updating headers
let existingTbody = '';
if (needsHeaderUpdate) {
existingTbody = $('.atfpp-glossary-table tbody').html();
}
let tableHeader = `
<table class="atfpp-glossary-table" data-default-lang="${savedSourceLang}">
<thead>
<tr>
<th colspan="2"></th>
${atfpp_glossary.atfpp_languages.filter(lang =>
!(singleLanguageMode && lang.code === singleLanguageCode)
).map(lang =>
`<th colspan="2" title="${lang.alt}" class="atfpp-lang-header atfpp-lang-col-${lang.code}" data-lang="${lang.code}">
${lang.flag ? lang.flag : `<img src="${lang.img}" alt="${lang.alt}" />`}
</th>`
).join('')}
<th class="atfpp-actions-cell">
<div class="atfpp-action-buttons-header">
<button class="atfpp-actions-header-btn" id="atfpp-actions-header-btn-left" title="Scroll Left">
<img src="${atfpp_glossary.url}admin/atfpp-dashboard/images/arrow-left.svg" />
</button>
<button class="atfpp-actions-header-btn" id="atfpp-actions-header-btn-right" title="Scroll Right">
<img src="${atfpp_glossary.url}admin/atfpp-dashboard/images/arrow-right.svg" />
</button>
</div>
</th>
</tr>
<tr>
<th>Glossary Entry</th>
<th>Type</th>
${atfpp_glossary.atfpp_languages.filter(lang =>
!(singleLanguageMode && lang.code === singleLanguageCode)
).map(lang =>
`<th colspan="2" data-lang="${lang.code}">${lang.alt}</th>`
).join('')}
<th colspan="2">Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
`;
$('.atfpp-glossary-table-wrapper').html(tableHeader);
// Apply current language filter column hiding to headers
const currentActiveLang = $('.atfpp-lang-filter-btn.active').data('lang');
if (currentActiveLang) {
$('.atfpp-glossary-table').find(`th[data-lang="${currentActiveLang}"]`).hide();
}
// Restore existing table body if we were updating headers
if (needsHeaderUpdate && existingTbody) {
$('.atfpp-glossary-table tbody').html(existingTbody);
// Need to update all existing rows to match new column structure
$('.atfpp-glossary-table tbody tr').each(function() {
const $row = $(this);
const rowOriginalLang = $row.data('original-language');
const rowTerm = $row.data('term');
// Skip edit rows
if ($row.hasClass('atfpp-glossary-edit-row')) return;
// Get existing translation data - EXCLUDE source terms to prevent mixing
const existingTranslations = {};
$row.find('[class*="atfpp-lang-col-"]').each(function() {
const $cell = $(this);
const langMatch = $cell.attr('class').match(/atfpp-lang-col-([a-zA-Z_-]+)/);
if (langMatch && langMatch[1]) {
const langCode = langMatch[1];
// Check if this cell contains a source term - DON'T extract source terms as translations
const hasSourceTerm = $cell.find('.atfpp-source-term').length > 0;
const isSourceByData = $cell.data('is-source') === true || $cell.data('is-source') === 'true';
// Only get actual translations, NOT source terms
if (!hasSourceTerm && !isSourceByData) {
const $translatedTerm = $cell.find('.atfpp-translated-term');
if ($translatedTerm.length) {
const translation = $translatedTerm.data('full-text') || $translatedTerm.text().trim();
if (translation && translation.trim() !== '') {
existingTranslations[langCode] = translation.trim();
}
}
}
}
});
// Rebuild row HTML with all language columns
let newTranslationsHtml = '';
atfpp_glossary.atfpp_languages.forEach(lang => {
const langCode = lang.code;
// Skip source language column if in single language mode
if (singleLanguageMode && langCode === singleLanguageCode) {
return;
}
if (langCode === rowOriginalLang) {
// Source language - show original term
newTranslationsHtml += `
<td colspan="2" class="atfpp-lang-col-${langCode}" data-lang="${langCode}" data-is-source="true">
<span class="atfpp-source-term">
${escapeHtml(rowTerm)}
</span>
</td>
`;
} else {
// Translation language
const existingTranslation = existingTranslations[langCode];
if (existingTranslation && existingTranslation.trim() !== '') {
// Has translation
const truncatedText = existingTranslation.length > 7 ? existingTranslation.substring(0, 7) + '…' : existingTranslation;
newTranslationsHtml += `
<td colspan="2" class="atfpp-lang-col-${langCode}" data-lang="${langCode}">
<span class="atfpp-translated-term"
data-full-text="${escapeHtml(existingTranslation)}"
title="${escapeHtml(existingTranslation)}">
${escapeHtml(truncatedText)}
</span>
</td>
`;
} else {
// No translation - show add button
newTranslationsHtml += `
<td colspan="2" class="atfpp-lang-col-${langCode}" data-lang="${langCode}">
<button type="button" class="atfpp-edit-btn-svg" data-term="${escapeHtml(rowTerm)}" data-source-lang="${escapeHtml(rowOriginalLang)}">
<img src="${atfpp_glossary.url}admin/atfpp-dashboard/images/file.svg" alt="Add Translation" />
</button>
</td>
`;
}
}
});
// Update the row HTML
const $termCell = $row.find('td:first');
const $typeCell = $row.find('td:nth-child(2)');
const $actionsCell = $row.find('.atfpp-actions-cell');
$row.html(`
${$termCell[0].outerHTML}
${$typeCell[0].outerHTML}
${newTranslationsHtml}
${$actionsCell[0].outerHTML}
`);
});
// Apply current language filter column hiding to restored rows
const currentActiveLang = $('.atfpp-lang-filter-btn.active').data('lang');
if (currentActiveLang) {
$('.atfpp-glossary-table tbody tr').each(function() {
$(this).find(`td[data-lang="${currentActiveLang}"][data-is-source="true"]`).hide();
});
}
}
}
// Append the new entry to the glossary table
$('.atfpp-glossary-table tbody').append(newEntryHtml);
// Apply current language filter column hiding to the new row
const currentActiveLang = $('.atfpp-lang-filter-btn.active').data('lang');
if (currentActiveLang) {
const $newRow = $('.atfpp-glossary-table tbody tr:last');
// Hide the source language column for the new row
$newRow.find(`td[data-lang="${currentActiveLang}"][data-is-source="true"]`).hide();
}
// Re-select the new table
$glossaryTable = $('.atfpp-glossary-table');
// Update the UI (show table, remove no-results, apply zebra striping, etc.)
$('.atfpp-glossary-table').show();
$('.atfpp-glossary-table-wrapper').show();
$('#atfpp-no-results').remove();
updateGlossaryTableVisibility();
applyZebraStriping();
// Update scroll button visibility
updateScrollButtonVisibility();
// Update language filter buttons, passing the saved language
updateLanguageFilterButtons(savedSourceLang);
// After adding, check if there are now two or more unique original languages and show filter bar if needed
const $rows = $('.atfpp-glossary-table tbody tr');
const uniqueLangs = {};
$rows.each(function() {
const lang = $(this).data('original-language');
if (lang) uniqueLangs[lang] = true;
});
const langCodes = Object.keys(uniqueLangs);
const $filterBar = $('.atfpp-language-filters');
// Remember the currently active filter before recreating buttons
const currentlyActiveLang = $('.atfpp-lang-filter-btn.active').data('lang');
if (langCodes.length > 1) {
// If filter bar is empty, create buttons for all unique languages
if ($filterBar.length === 0) {
// Insert filter bar after controls if not present
$('<div class="atfpp-language-filters"></div>').insertAfter('.atfpp-alphabet');
}
const $newFilterBar = $('.atfpp-language-filters');
$newFilterBar.empty();
langCodes.forEach(function(code, i) {
const langObj = (atfpp_glossary.atfpp_languages || []).find(l => l.code === code);
const label = langObj ? (langObj.alt + ' Terms') : (code + ' Terms');
const flag = langObj && langObj.img ? `<img src="${langObj.img}" alt="${langObj.alt}" /> ` : '';
// Preserve the currently active filter, or default to the newly added language if no active filter
const shouldBeActive = currentlyActiveLang === code || (!currentlyActiveLang && code === savedSourceLang);
$newFilterBar.append(`
<button class="atfpp-lang-filter-btn${shouldBeActive ? ' active' : ''}" data-lang="${code}">
${flag}${label}
</button>
`);
});
$newFilterBar.show();
} else {
// If only one language, hide filter bar
$filterBar.empty().hide();
}
// Reapply the current language filter to ensure the new entry is visible
const activeLang = $('.atfpp-lang-filter-btn.active').data('lang');
if (activeLang) {
// Trigger the language filter click to properly apply column hiding
$('.atfpp-lang-filter-btn.active').trigger('click');
}
// Update alphabet buttons
updateAlphabetButtonStates();
} else {
// Fallback: Success but no server data - use original method with request data
console.warn('Success but no server data returned, using fallback method');
$form.hide();
$('#add-glossary-success').removeClass('atfpp-hidden');
// Could add fallback logic here if needed, or just show success
}
} else {
alert(resp.data?.message || resp.data || 'Unknown error');
}
});
});
// Function to update language filter buttons
function updateLanguageFilterButtons(originalLang) {
if (!originalLang) return;
const $filters = $('.atfpp-language-filters');
// Check if a button for this language already exists in the filter
if ($filters.find('.atfpp-lang-filter-btn[data-lang="' + originalLang + '"]').length === 0) {
// Find the language object for label and flag
let langObj = (atfpp_glossary.atfpp_languages || []).find(l => l.code === originalLang);
let label = langObj ? (langObj.alt + ' Terms') : (originalLang + ' Terms');
let flag = langObj && langObj.img ? `<img src="${langObj.img}" alt="${langObj.alt}" /> ` : '';
$filters.append(`
<button class="atfpp-lang-filter-btn" data-lang="${originalLang}">
${flag}${label}
</button>
`);
}
// After append, check total number of filter buttons
const totalBtns = $filters.find('.atfpp-lang-filter-btn').length;
if (totalBtns <= 1) {
// If only one, remove all (hide filter bar)
$filters.empty();
}
}
// Add close button handler for success message
$('#add-glossary-success').on('click', '#atfpp-glossary-success-close', function() {
$('#atfpp-glossary-modal-add').addClass('atfpp-hidden').removeClass('active');
$('#atfpp-glossary-modal-add').find('form').show();
$('#add-glossary-success').addClass('atfpp-hidden');
});
// --- GLOSSARY SEARCH FUNCTIONALITY ---
$(document).on('input', '.atfpp-search', function() {
const search = $(this).val().toLowerCase().trim();
// First apply search filter
$glossaryTable.find('tbody tr').each(function() {
const $row = $(this);
const term = $row.find('.atfpp-entry-title').text().toLowerCase();
const desc = $row.find('.atfpp-entry-desc').text().toLowerCase();
const showBySearch = !search || term.indexOf(search) !== -1 || desc.indexOf(search) !== -1;
$row.toggle(showBySearch);
});
if (search !== '') {
$('.atfpp-glossary-table-wrapper').show();
$('#atfpp-no-results').remove();
}else{
$('.atfpp-glossary-table-wrapper').show();
$('#atfpp-no-results').remove();
}
applyZebraStriping();
filterGlossaryRows();
// Update UI visibility
updateGlossaryTableVisibility();
});
// --- GLOSSARY TYPE FILTER FUNCTIONALITY ---
$(document).on('change', '.atfpp-glossary-type', function() {
var selectedType = $(this).val();
if (selectedType) {
$('.atfpp-glossary-table-wrapper').show();
$('#atfpp-no-results').remove();
}
filterGlossaryRows();
applyZebraStriping();
});
// Export Glossary Button - Fix double download issue
$(document).off('click', '.atfpp-export-btn').on('click', '.atfpp-export-btn', function(e) {
e.preventDefault();
e.stopPropagation(); // Prevent event bubbling
// Create a temporary link to trigger the download
var url = atfpp_glossary.ajaxurl + '?action=atfpp_export_glossary';
var link = document.createElement('a');
link.href = url;
link.download = 'glossary-export.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
// Optionally, also close on "Cancel" in the modal
$(document).on('click', '.atfpp-glossary-modal-actions-left', function() {
$(this).closest('.atfpp-glossary-modal').addClass('atfpp-hidden').removeClass('active');
});
// Add cancel edit handler
$glossaryTable.on('click', '.atfpp-cancel-edit-btn', function(e) {
e.preventDefault();
e.stopPropagation();
const $editRow = $(this).closest('tr.atfpp-glossary-edit-row');
const $originalRow = $editRow.prev('tr');
$originalRow.show();
$editRow.remove();
});
// Add this function after the existing functions
function updateActionsHeaderVisibility() {
const $tableWrapper = $('.atfpp-glossary-table-wrapper');
const $table = $tableWrapper.find('.atfpp-glossary-table');
const $actionsHeader = $('.atfpp-actions-header-btn').closest('th');
// Only proceed if the table exists
if ($table.length === 0 || !$table[0]) {
$actionsHeader.hide();
return;
}
// Check if table has horizontal scroll
const hasHorizontalScroll = $table[0].scrollWidth > $tableWrapper[0].clientWidth;
// Show/hide actions header based on scroll
$actionsHeader.toggle(hasHorizontalScroll);
}
// Initial check for actions header visibility
updateActionsHeaderVisibility();
// Update on window resize
$(window).on('resize', _.debounce(function() {
updateActionsHeaderVisibility();
}, 250));
// Update after any content changes that might affect table width
const observer = new MutationObserver(_.debounce(function() {
updateActionsHeaderVisibility();
}, 250));
// Observe the table wrapper for changes
const $tableWrapper = $('.atfpp-glossary-table-wrapper');
if ($tableWrapper.length) {
observer.observe($tableWrapper[0], {
childList: true,
subtree: true,
attributes: true
});
}
// Function to update button visibility based on scroll position
function updateScrollButtonVisibility() {
const $wrapper = $('.atfpp-glossary-table-wrapper');
// Check if wrapper exists
if (!$wrapper.length) {
return;
}
// Check if wrapper has content
if (!$wrapper[0]) {
return;
}
const scrollLeft = $wrapper.scrollLeft();
const scrollWidth = $wrapper[0].scrollWidth;
const clientWidth = $wrapper[0].clientWidth;
// Hide left button if at the leftmost position
if (scrollLeft === 0) {
$('#atfpp-actions-header-btn-left').css('visibility', 'hidden');
} else {
$('#atfpp-actions-header-btn-left').css('visibility', 'visible');
}
if (scrollLeft + clientWidth >= scrollWidth) {
$('#atfpp-actions-header-btn-right').css('visibility', 'hidden'); // Use hide() to remove from layout
} else {
$('#atfpp-actions-header-btn-right').css('visibility', 'visible'); // Use show() to display
}
}
// Call this function on page load
$(document).ready(function() {
updateScrollButtonVisibility();
});
// Call this function after scrolling
$glossaryTable.on('scroll', function() {
updateScrollButtonVisibility();
});
// Scroll table to the right when actions header button is clicked
$('.atfpp-glossary-table-wrapper').off('click', '#atfpp-actions-header-btn-right').on('click', '#atfpp-actions-header-btn-right', function(e) {
e.preventDefault();
const $wrapper = $(this).closest('.atfpp-glossary-table-wrapper');
const scrollAmount = 300;
$wrapper.animate({
scrollLeft: $wrapper.scrollLeft() + scrollAmount
}, 400, updateScrollButtonVisibility);
});
// Scroll table to the left when actions header button is clicked
$('.atfpp-glossary-table-wrapper').off('click', '#atfpp-actions-header-btn-left').on('click', '#atfpp-actions-header-btn-left', function(e) {
e.preventDefault();
const $wrapper = $(this).closest('.atfpp-glossary-table-wrapper');
const scrollAmount = 300;
$wrapper.animate({
scrollLeft: $wrapper.scrollLeft() - scrollAmount
}, 400, updateScrollButtonVisibility);
});
// Close handler for import success message
$(document).on('click', '.atfpp-import-close-btn', function() {
// Hide the import success UI
$('#atfpp-import-success-ui').addClass('atfpp-hidden');
// Optionally, also close the modal
$('#atfpp-glossary-modal-import').addClass('atfpp-hidden');
window.location.reload();
});
// Add input event handler for add form fields
$addGlossaryForm.on('input', '.atfpp-add-term, .atfpp-add-desc', function() {
const $textarea = $(this);
const $error = $textarea.next('.atfpp-translation-error');
const value = $textarea.val().trim();
const maxLength = 240;
// Check for invalid characters (like script tags)
if (value.includes('<') || value.includes('>')) {
$textarea.addClass('error');
$error.text('Invalid input').show();
return; // Stop further validation for this field
}
if (value.length > maxLength) {
$textarea.addClass('error');
$error.text(`Must be less than ${maxLength} characters`).show();
} else {
$textarea.removeClass('error');
$error.hide();
}
});
});