';
loadWordCounts(catId, poetId, remStopWords);
}
function switchTabWordsForPoem(evt, tabId, id, catId, poetId) {
var loadButton = document.getElementById("load-word-counts");
if (loadButton != null) {
loadButton.remove();
countPoemWords(id, catId, poetId);
}
switchTab(evt, tabId);
}
function switchTabWords(evt, tabId, catId, poetId) {
var loadButton = document.getElementById("load-word-counts");
if (loadButton != null) {
loadButton.remove();
loadWordCounts(catId, poetId, false);
}
switchTab(evt, tabId);
}
function loadWordCounts(catId, poetId, remStopWords) {
var divParent = document.getElementById('wordcounts-placeholder');
var imgElementId = 'loadingwordcountsimg';
divParent.innerHTML = divParent.innerHTML + '
';
$.ajax({
type: "GET",
url: '?Handler=CategoryWordCounts&catId=' + String(catId) + '&poetId=' + String(poetId) + '&remStopWords=' + remStopWords.toString(),
error: function () {
if (document.getElementById("remove-this-wordcounts") != null) {
document.getElementById("remove-this-wordcounts").remove();
}
},
success: function (data) {
if (document.getElementById("remove-this-wordcounts") != null) {
document.getElementById("remove-this-wordcounts").remove();
if (document.getElementById("load-word-counts") != null) {
document.getElementById("load-word-counts").remove();
}
$(data).appendTo(divParent);
plotChart('words-stats');
}
},
});
}
function onSearchWordCounts(catId, poetId, totalWordCount) {
setTimeout(function () {
var value = document.getElementById('wordcountterm').value;
if (value != null && value.trim() != '') {
document.getElementById('remStopWords').style.display = 'none'
}
else {
document.getElementById('remStopWords').style.display = 'block'
}
var divParent = document.getElementById('wordcounts-table');
var imgElementId = 'loadingwordcountsimg';
divParent.innerHTML = '
';
$.ajax({
type: "GET",
url: '?Handler=SearchCategoryWordCounts&catId=' + String(catId) + '&poetId=' + String(poetId) + '&totalWordCount=' + String(totalWordCount) + '&term=' + value,
error: function () {
if (document.getElementById("remove-this-wordcounts") != null) {
document.getElementById("remove-this-wordcounts").remove();
}
},
success: function (data) {
if (document.getElementById("remove-this-wordcounts") != null) {
document.getElementById("remove-this-wordcounts").remove();
$(data).appendTo(divParent);
plotChart('words-stats');
}
},
});
}, 500);
}
function loadWordCountsByCat(term, poetId, catId, blur) {
var divParent = document.getElementById('wordcounts-placeholder');
var imgElementId = 'loadingwordcountsimg';
divParent.innerHTML = divParent.innerHTML + '
';
$.ajax({
type: "GET",
url: '?Handler=WordCountsByPoet&term=' + term + '&poetId=' + poetId.toString() + '&catId=' + catId.toString() + '&blur=' + blur.toString(),
error: function () {
if (document.getElementById("remove-this-wordcounts") != null) {
document.getElementById("remove-this-wordcounts").remove();
}
},
success: function (data) {
if (document.getElementById("remove-this-wordcounts") != null) {
document.getElementById("remove-this-wordcounts").remove();
if (document.getElementById("load-word-counts") != null) {
document.getElementById("load-word-counts").style.display = 'none';
}
$(data).appendTo(divParent);
plotChart('words-stats');
}
},
});
}
function countPoemWords(poemId) {
var loadButton = document.getElementById("load-word-counts");
if (loadButton != null) {
loadButton.remove();
}
var divParent = document.getElementById('wordcounts-placeholder');
var imgElementId = 'loadingwordcountsimg';
divParent.innerHTML = divParent.innerHTML + '
';
$.ajax({
type: "GET",
url: '?Handler=PoemWordCounts&poemId=' + String(poemId),
error: function () {
if (document.getElementById("remove-this-wordcounts") != null) {
document.getElementById("remove-this-wordcounts").remove();
}
},
success: function (data) {
if (document.getElementById("remove-this-wordcounts") != null) {
document.getElementById("remove-this-wordcounts").remove();
divParent.innerHTML = '
'
$(data).appendTo(divParent);
plotChart('words-stats');
}
},
});
}
function switchToCatTab(evt, tabId, catId) {
loadCatRecitations(catId);
switchTab(evt, tabId);
}
function switchTab(evt, tabId) {
// Declare all variables
var i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(tabId).style.display = "block";
evt.currentTarget.className += " active";
}
function persianToEnglishNumber(str) {
str = str.replace(/٬/g, '');
const persianDigits = '۰۱۲۳۴۵۶۷۸۹';
return str.replace(/[۰-۹]/g, d => persianDigits.indexOf(d));
}
function toPersianNumber(n) {
const persianDigits = '۰۱۲۳۴۵۶۷۸۹';
return String(n).replace(/[0-9]/g, d => persianDigits[d]);
}
function plotChart(tableId, maxCols = 9) {
let table = document.getElementById(tableId);
if (!table) return;
let headerCols = table.querySelector("thead tr").children;
let xTitle = headerCols[1].innerText.trim();
let yTitle = headerCols[2].innerText.trim();
// collect every eligible row first - a table's own displayed row order can be meaningful on
// the page itself (e.g. sub-categories kept in their natural/defined order rather than sorted
// by count), so that order is never disturbed here; sorting/truncating for the chart only
// happens below, on a separate copy
let rows = [];
table.querySelectorAll("tbody tr").forEach(row => {
let cols = row.querySelectorAll("td");
if (cols.length >= 3) {
let rowNumber = persianToEnglishNumber(cols[0].innerText.trim());
if (rowNumber === "0") return; // Ignore rows with row number ۰
let xValue = persianToEnglishNumber(cols[1].innerText.trim());
let yValue = persianToEnglishNumber(cols[2].innerText.trim());
rows.push({ label: xValue, value: parseInt(yValue) });
}
});
// a table opts into charting its top values by count - rather than whatever its own row order
// happens to be - via data-chart-sort="value-desc" on the
itself
if (table.dataset.chartSort === "value-desc") {
rows.sort((a, b) => b.value - a.value);
}
rows = rows.slice(0, maxCols + 1);
let labels = rows.map(r => r.label);
let values = rows.map(r => r.value);
let existingCanvas = document.getElementById(`chart-${tableId}`);
if (existingCanvas) {
existingCanvas.remove();
}
let canvas = document.createElement("canvas");
canvas.id = `chart-${tableId}`;
table.parentNode.insertBefore(canvas, table.nextSibling);
const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const textColor = isDark ? '#ffffff' : '#333333';
new Chart(canvas, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: yTitle,
data: values,
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
x: {
title: {
display: true,
text: xTitle,
font: { family: 'Vazirmatn', size: 14 },
color: textColor
},
ticks: {
font: { family: 'Vazirmatn', size: 12 },
color: textColor
}
},
y: {
title: {
display: true,
text: yTitle,
font: { family: 'Vazirmatn', size: 14 },
color: textColor
},
ticks: {
font: { family: 'Vazirmatn', size: 12 },
color: textColor
},
beginAtZero: true,
}
},
plugins: {
legend: {
display: false,
},
},
layout: {
padding: 10
}
}
});
}
function deletePoetFromSearch(poetId, poetName) {
if (!confirm('آیا میخواهید ' + poetName + ' را از نتایج حذف کنید؟')) return;
const currentUrl = new URL(window.location.href);
const params = currentUrl.searchParams;
params.append('e', poetId.toString());
currentUrl.search = params.toString();
window.location.href = currentUrl.toString();
}
function loadContributions(dataType) {
var divParent = document.getElementById('days-placeholder');
divParent.innerHTML = ' ';
$.ajax({
type: "GET",
url: '?Handler=GroupedByDate&dataType=' + dataType,
error: function (e) {
if (document.getElementById("remove-days-placeholder") != null) {
document.getElementById("remove-days-placeholder").remove();
}
if (e.responseText == null)
alert(e);
else
alert(e.responseText);
},
success: function (data) {
if (document.getElementById("remove-days-placeholder") != null) {
document.getElementById("remove-days-placeholder").remove();
$(data).appendTo(divParent);
plotChart('grouped-by-date', 100);
plotChart('grouped-by-user', 100);
var elements = document.querySelectorAll('.poemtablinks.active');
for (var i = 0; i < elements.length; i++) {
elements[i].classList.remove('active');
}
var dataTypeButton = document.getElementById(dataType);
if (dataTypeButton) {
dataTypeButton.classList.add('active');
}
}
},
});
}
function loadContributionsDays(dataType, pageNumber) {
var divParent = document.getElementById('days');
divParent.innerHTML = ' ';
$.ajax({
type: "GET",
url: '?Handler=GroupedByDay&dataType=' + dataType + '&pageNumber=' + pageNumber.toString(),
error: function (e) {
if (document.getElementById("remove-days-placeholder") != null) {
document.getElementById("remove-days-placeholder").remove();
}
if (e.responseText == null)
alert(e);
else
alert(e.responseText);
},
success: function (data) {
if (document.getElementById("remove-days-placeholder") != null) {
document.getElementById("remove-days-placeholder").remove();
$(data).appendTo(divParent);
plotChart('grouped-by-date', 100);
}
},
});
}
function loadContributionsUsers(dataType, pageNumber) {
var divParent = document.getElementById('usrs');
divParent.innerHTML = ' ';
$.ajax({
type: "GET",
url: '?Handler=GroupedByUsers&dataType=' + dataType + '&pageNumber=' + pageNumber.toString(),
error: function (e) {
if (document.getElementById("remove-days-placeholder") != null) {
document.getElementById("remove-days-placeholder").remove();
}
if (e.responseText == null)
alert(e);
else
alert(e.responseText);
},
success: function (data) {
if (document.getElementById("remove-days-placeholder") != null) {
document.getElementById("remove-days-placeholder").remove();
$(data).appendTo(divParent);
plotChart('grouped-by-user', 100);
}
},
});
}
function loadTopVisits(url) {
if (document.getElementById("top-visits") != null) {
return;
}
if (document.getElementById("load-topvisits") != null) {
document.getElementById("load-topvisits").remove();
}
var divParent = document.getElementById('topvisits-placeholder');
var imgElementId = 'loadingtopvisitsimg';
divParent.innerHTML = ' ';
$.ajax({
type: "GET",
url: '?Handler=TopVisits&url=' + url,
error: function () {
if (document.getElementById("remove-this-topvisits") != null) {
document.getElementById("remove-this-topvisits").remove();
}
},
success: function (data) {
if (document.getElementById("remove-this-topvisits") != null) {
document.getElementById("remove-this-topvisits").remove();
$(data).appendTo(divParent);
plotChart('top-visits');
}
},
});
}
function load7DaysVisits(url) {
if (document.getElementById("7days-visits") != null) {
return;
}
if (document.getElementById("load-7daysvisits") != null) {
document.getElementById("load-7daysvisits").remove();
}
var divParent = document.getElementById('7daysvisits-placeholder');
var imgElementId = 'loading7daysvisitsimg';
divParent.innerHTML = ' ';
$.ajax({
type: "GET",
url: '?Handler=SevenDaysVisits&url=' + url,
error: function () {
if (document.getElementById("remove-this-7daysvisits") != null) {
document.getElementById("remove-this-7daysvisits").remove();
}
},
success: function (data) {
if (document.getElementById("remove-this-7daysvisits") != null) {
document.getElementById("remove-this-7daysvisits").remove();
$(data).appendTo(divParent);
plotChart('7days-visits');
}
},
});
}
// Turns a plain text into a "search as you type" picker over a small
// in-memory location catalog ({id, name, latitude, longitude}[]), instead of a
// long the user has to scroll/search through by hand. If nothing in
// the catalog matches what's typed, the dropdown offers to define it as a new
// location instead - see onNotFound.
// searchInput - the visible text the user types a location name into
// hiddenInput - an existing/hidden that ends up holding the selected
// location's Id as a string (cleared, i.e. '', whenever the
// text doesn't currently match a selected location) - callers
// that used to read a 's .val() can keep doing so
// unchanged as long as this element keeps the same id
// resultsBox - an empty container element (positioned by .up-autocomplete-results)
// right after searchInput, used to render the dropdown
// locations - array of {id, name, latitude, longitude}
// onSelect(loc) - called with the chosen location object, or null when the
// field is cleared/typed into (so any "selected" state a
// caller is tracking elsewhere - like a new-location panel
// that should hide - can be kept in sync)
// onNotFound(text) - called with the trimmed typed text when it matches no
// location in the catalog and the user clicks the "define
// as new location" row
function setupLocationAutocomplete(searchInput, hiddenInput, resultsBox, locations, onSelect, onNotFound) {
var MAX_RESULTS = 12;
function hide() {
resultsBox.style.display = 'none';
resultsBox.innerHTML = '';
}
function selectLocation(loc) {
searchInput.value = loc.name;
hiddenInput.value = String(loc.id);
hide();
if (onSelect) onSelect(loc);
}
function render(typedText) {
var typedLower = typedText.toLowerCase();
var matches = (locations || []).filter(function (l) {
return l.name && l.name.toLowerCase().indexOf(typedLower) !== -1;
});
if (matches.length == 0) {
var emptyRow = document.createElement('div');
emptyRow.className = 'up-autocomplete-item up-autocomplete-item--empty';
emptyRow.textContent = '➕ «' + typedText + '» در فهرست پیدا نشد؛ بهعنوان مکان جدید تعریف شود';
emptyRow.addEventListener('click', function () {
hide();
if (onNotFound) onNotFound(typedText);
});
resultsBox.innerHTML = '';
resultsBox.appendChild(emptyRow);
resultsBox.style.display = 'block';
return;
}
resultsBox.innerHTML = '';
for (var i = 0; i < Math.min(matches.length, MAX_RESULTS); i++) {
var loc = matches[i];
var row = document.createElement('div');
row.className = 'up-autocomplete-item';
row.textContent = loc.name + ' (' + loc.latitude + ', ' + loc.longitude + ')';
row.addEventListener('click', function (l) {
return function () { selectLocation(l); };
}(loc));
resultsBox.appendChild(row);
}
if (matches.length > MAX_RESULTS) {
var moreRow = document.createElement('div');
moreRow.className = 'up-autocomplete-item up-autocomplete-item--more';
moreRow.textContent = '… و ' + (matches.length - MAX_RESULTS) + ' مورد دیگر (برای محدودکردن نتایج بیشتر تایپ کنید)';
resultsBox.appendChild(moreRow);
}
resultsBox.style.display = 'block';
}
searchInput.addEventListener('input', function () {
hiddenInput.value = ''; // typing invalidates whatever was previously selected
var typed = searchInput.value.trim();
if (typed == '') {
hide();
if (onSelect) onSelect(null);
return;
}
render(typed);
});
searchInput.addEventListener('focus', function () {
var typed = searchInput.value.trim();
if (typed != '' && hiddenInput.value == '') {
render(typed); // re-show suggestions for text that hasn't resolved to a pick yet
}
});
document.addEventListener('click', function (e) {
if (e.target !== searchInput && !resultsBox.contains(e.target)) {
hide();
}
});
}
// same search-as-you-type / "not found -> define new" pattern as setupLocationAutocomplete above,
// but for picking a GanjoorRelatedPerson (people don't have coordinates, so the result rows and the
// "not found" wording differ enough that sharing one function would need special-casing throughout)
function setupPersonAutocomplete(searchInput, hiddenInput, resultsBox, people, onSelect, onNotFound) {
var MAX_RESULTS = 12;
// The birth/death-year subtitle in each dropdown row below helps tell two same-named people
// apart (the Shahnameh/mythological person catalog in particular has several), but it's not
// always enough - two people can share both a name and era, or the subtitle can be blank if
// neither year is recorded. So once a pick is made (or the field already carries one from the
// server, e.g. correcting an existing suggestion), show a small "👁 view full info" link right
// next to the box, opening that exact person's full profile (PersonWindow.open) so whoever is
// picking - or a moderator reviewing someone else's pick - can be sure it's the right person,
// not just one with a matching name/years.
var viewLink = document.createElement('a');
viewLink.href = 'javascript:void(0)';
viewLink.className = 'up-icon-btn';
viewLink.title = 'مشاهدهٔ اطلاعات کامل این شخصیت';
viewLink.setAttribute('aria-label', 'مشاهدهٔ اطلاعات کامل این شخصیت');
viewLink.style.marginRight = '4px';
viewLink.textContent = '👁';
viewLink.addEventListener('click', function () {
var id = parseInt(hiddenInput.value, 10);
if (id) PersonWindow.open(id);
});
if (searchInput.parentNode) {
searchInput.parentNode.insertBefore(viewLink, searchInput.nextSibling);
}
function updateViewLink() {
var id = parseInt(hiddenInput.value, 10);
viewLink.style.display = id ? 'inline' : 'none';
}
updateViewLink(); // the hidden input may already carry a value set by the server (editing/correcting an existing pick)
function hide() {
resultsBox.style.display = 'none';
resultsBox.innerHTML = '';
}
function personSubtitle(p) {
if (p.birthYearInLHijri && p.deathYearInLHijri) {
return p.birthYearInLHijri + ' - ' + p.deathYearInLHijri;
}
if (p.birthYearInLHijri) {
return 'تولد ' + p.birthYearInLHijri;
}
if (p.deathYearInLHijri) {
return 'وفات ' + p.deathYearInLHijri;
}
return '';
}
function selectPerson(p) {
searchInput.value = p.name;
hiddenInput.value = String(p.id);
updateViewLink();
hide();
if (onSelect) onSelect(p);
}
function render(typedText) {
var typedLower = typedText.toLowerCase();
var matches = (people || []).filter(function (p) {
return p.name && p.name.toLowerCase().indexOf(typedLower) !== -1;
});
if (matches.length == 0) {
var emptyRow = document.createElement('div');
emptyRow.className = 'up-autocomplete-item up-autocomplete-item--empty';
emptyRow.textContent = '➕ «' + typedText + '» در فهرست پیدا نشد؛ بهعنوان شخصیت جدید تعریف شود';
emptyRow.addEventListener('click', function () {
hide();
if (onNotFound) onNotFound(typedText);
});
resultsBox.innerHTML = '';
resultsBox.appendChild(emptyRow);
resultsBox.style.display = 'block';
return;
}
resultsBox.innerHTML = '';
for (var i = 0; i < Math.min(matches.length, MAX_RESULTS); i++) {
var p = matches[i];
var subtitle = personSubtitle(p);
var row = document.createElement('div');
row.className = 'up-autocomplete-item';
row.textContent = subtitle ? (p.name + ' (' + subtitle + ')') : p.name;
row.addEventListener('click', function (person) {
return function () { selectPerson(person); };
}(p));
resultsBox.appendChild(row);
}
if (matches.length > MAX_RESULTS) {
var moreRow = document.createElement('div');
moreRow.className = 'up-autocomplete-item up-autocomplete-item--more';
moreRow.textContent = '… و ' + (matches.length - MAX_RESULTS) + ' مورد دیگر (برای محدودکردن نتایج بیشتر تایپ کنید)';
resultsBox.appendChild(moreRow);
}
resultsBox.style.display = 'block';
}
searchInput.addEventListener('input', function () {
hiddenInput.value = ''; // typing invalidates whatever was previously selected
updateViewLink();
var typed = searchInput.value.trim();
if (typed == '') {
hide();
if (onSelect) onSelect(null);
return;
}
render(typed);
});
searchInput.addEventListener('focus', function () {
var typed = searchInput.value.trim();
if (typed != '' && hiddenInput.value == '') {
render(typed); // re-show suggestions for text that hasn't resolved to a pick yet
}
});
document.addEventListener('click', function (e) {
if (e.target !== searchInput && !resultsBox.contains(e.target)) {
hide();
}
});
}