divan/GanjooRazor/wwwroot/js/bk.js
Anas Rashid 1811572d9e Divan: Urdu classical poetry fork of GanjoorService
- Rebrand گنجور -> دیوان across GanjooRazor; lang=ur; Urdu home page and footer
- Hijri century groups with Urdu names
- Noto Nastaliq Urdu (default) / Noto Naskh Arabic switch
- Remove footer links to Ganjoor-only services; link Wikisource, data and code
- Linux deployment: Dockerfile, docker-compose (SQL Server 2022, API, site, Caddy)
- DIVAN.md: changes and deploy guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:14:25 +02:00

3019 lines
117 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// --- CSRF (antiforgery) token for AJAX calls --------------------------------------------
// Every POST/PUT/DELETE handler in the app validates an antiforgery token by default. A real
// <form> submission gets that token for free (the asp-* form tag helper / @Html.AntiForgeryToken()
// injects a hidden field automatically), but the many $.ajax(...) calls in this file talk to
// those same handlers directly with no <form> involved, so they need to attach the token
// themselves. The token value is rendered once per page as <meta name="csrf-token" ...> (see
// _Layout.cshtml / _UserPanelLayout.cshtml / _AdminLayout.cshtml and the couple of standalone
// pages that make their own $.ajax calls), and this reads it once and attaches it to every
// jQuery AJAX request as a header from here on - so individual $.ajax(...) calls below don't
// need to be touched one by one. A page that doesn't render the meta tag (i.e. has nothing that
// needs it) simply sends no header, which is harmless.
(function () {
var token = $('meta[name="csrf-token"]').attr('content');
if (token) {
$.ajaxSetup({
headers: { 'X-CSRF-TOKEN': token }
});
}
})();
// --- Sanitizer "text was dropped" feedback ----------------------------------------------
// Shared by every place a TinyMCE-edited field (a comment, a bookmark note, a suggested
// poet spec-line, ...) gets rejected because the server's HTML sanitizer had to drop real
// text (see GanjoorService._BuildSanitizerDroppedTextError on the API side). Instead of just
// showing a generic error, this shows the user - highlighted, using the same diffChars utility
// already used elsewhere in this file for comparing correction blocks - exactly which part of
// what they typed would be dropped and why. That's almost always because they pasted the text
// in from somewhere else (Word, a chat app, a web page) that silently carried invalid/broken
// formatting along with it, so the message says that rather than talking about "tags" or the
// "<"/">" characters ordinary users don't recognize and have usually never typed themselves.
function _htmlToPlainText(html) {
var tmp = document.createElement('div');
tmp.innerHTML = html || '';
return (tmp.textContent || tmp.innerText || '').replace(/\s+/g, ' ').trim();
}
function _ensureJsDiffLoaded(callback) {
if (window.JsDiff) {
callback();
return;
}
var script = document.createElement('script');
script.src = '/lib/diff.js';
script.onload = callback;
document.head.appendChild(script);
}
function showSanitizerTextDroppedPopup(message, originalHtml, remainingPlainText) {
_ensureJsDiffLoaded(function () {
var originalPlainText = _htmlToPlainText(originalHtml);
var diff = JsDiff.diffChars(originalPlainText, remainingPlainText || '');
var fragment = document.createDocumentFragment();
for (var i = 0; i < diff.length; i++) {
if (diff[i].added) {
continue; // sanitizing only removes content, nothing to highlight as "added"
}
var node;
if (diff[i].removed) {
node = document.createElement('del');
node.style.color = 'red';
node.appendChild(document.createTextNode(diff[i].value));
} else {
node = document.createTextNode(diff[i].value);
}
fragment.appendChild(node);
}
var existing = document.getElementById('sanitizer-warning-modal');
if (existing) {
existing.parentNode.removeChild(existing);
}
var isDark = (window.matchMedia("(prefers-color-scheme: dark)").matches && localStorage.getItem("scheme") != "light") || localStorage.getItem("scheme") == "dark";
var overlay = document.createElement('div');
overlay.id = 'sanitizer-warning-modal';
overlay.style.cssText = 'display:block;position:fixed;z-index:9999;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgba(0,0,0,0.5);';
overlay.onclick = function (e) {
if (e.target === overlay) {
overlay.parentNode.removeChild(overlay);
}
};
var box = document.createElement('div');
box.style.cssText = 'margin:8% auto;padding:20px;max-width:600px;border-radius:8px;direction:rtl;text-align:right;' +
(isDark ? 'background-color:#222;color:#eee;' : 'background-color:#fff;color:#000;');
var messageP = document.createElement('p');
messageP.textContent = message;
box.appendChild(messageP);
if (fragment.childNodes.length > 0) {
var diffTitle = document.createElement('p');
var strong = document.createElement('strong');
strong.textContent = 'بخشی که حذف خواهد شد با رنگ قرمز و خط‌خورده مشخص شده است:';
diffTitle.appendChild(strong);
box.appendChild(diffTitle);
var diffBox = document.createElement('div');
diffBox.style.cssText = 'border:1px solid #999;padding:10px;border-radius:4px;margin-bottom:12px;white-space:pre-wrap;';
diffBox.appendChild(fragment);
box.appendChild(diffBox);
}
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.textContent = 'بستن';
closeBtn.onclick = function () {
overlay.parentNode.removeChild(overlay);
};
box.appendChild(closeBtn);
overlay.appendChild(box);
document.body.appendChild(overlay);
});
}
// Parses a raw error payload (jQuery's xhr.responseText, or the text of a server-rendered
// error block) looking for the sanitizer-text-dropped shape. Returns the parsed
// {message, remainingText} or null for any ordinary error (including plain, non-JSON text,
// which is the common case).
function _parseSanitizerDroppedTextError(raw) {
if (typeof raw !== 'string' || raw.length === 0) return null;
try {
var obj = JSON.parse(raw);
if (obj && typeof obj === 'object' && obj.sanitizerTextDropped === true) {
return obj;
}
} catch (e) {
// not JSON - an ordinary plain-text error, nothing to do here
}
return null;
}
// For AJAX (BadRequestObjectResult) handlers: xhr is jQuery's jqXHR from an error callback.
// originalHtml is the HTML the user actually submitted (for the diff). Returns true if it
// showed the popup (nothing else to do), or false for an ordinary error (caller's normal
// error handling, if any, still applies).
function tryShowSanitizerErrorFromXhr(xhr, originalHtml) {
var parsed = _parseSanitizerDroppedTextError(xhr && xhr.responseText);
if (parsed == null) return false;
showSanitizerTextDroppedPopup(parsed.message, originalHtml, parsed.remainingText);
return true;
}
// For server-rendered-partial handlers (postComment, postReplyComment, suggestNote): call this
// on the just-received (not yet necessarily inserted, though it's fine either way) root
// element of the rendered partial. If it carries the sanitizer-drop marker, this removes that
// error block from it (the popup below covers it, so leaving the same message sitting inline
// in the page too would just be clutter) and shows the popup. originalHtml is the HTML the user
// actually submitted (for the diff). Returns true if it handled a sanitizer-drop error.
// $rendered is the jQuery-wrapped set from $(data) for a just-received server-rendered
// partial (postComment, postReplyComment, suggestNote) - using jQuery's own filter/find here
// rather than indexing into the raw DOM nodes is what makes this robust to Razor's rendered
// HTML having a leading/trailing whitespace text node ahead of the actual element, which is
// common and would otherwise make a plain "first DOM node" check miss the real element.
// Returns true when the error div IS the partial's root element (true for every partial that
// currently marks it this way) - callers should skip inserting $rendered into the page at all
// in that case, since the popup this shows already covers the same message plus the diff.
function checkAppendedHtmlForSanitizerError($rendered, originalHtml) {
var $rootMatch = $rendered.filter('[data-sanitizer-remaining-text]');
var $errorDiv = $rootMatch.length > 0 ? $rootMatch : $rendered.find('[data-sanitizer-remaining-text]');
if ($errorDiv.length === 0) return false;
var remainingText = $errorDiv.attr('data-sanitizer-remaining-text');
if (!remainingText) return false;
var message = $errorDiv.text().trim();
if ($rootMatch.length === 0) {
$errorDiv.remove();
}
showSanitizerTextDroppedPopup(message, originalHtml, remainingText);
return true;
}
// From David Flanagan's "JavaScript: The Definitive Guide" 5th Ed,
// http://www.davidflanagan.com/javascript5/display.php?n=15-4&f=15/04.js
//modified 4 ganjoor a little bit
function isMember(element, classname) {
var classes = element.className;
if (!classes) return false;
if (classes == classname) return true;
var whitespace = /\s+/;
if (!whitespace.test(classes)) return false;
if (typeof classes != 'string') return false;
var c = classes.split(whitespace);
for (var i = 0; i < c.length; i++) {
if (c[i] == classname) return true;
}
return false;
}
function getElements(classname, classname2, classname3, classname4, tagname, root) {
if (!root) root = document;
else if (typeof root == "string") root = document.getElementById(root);
if (!tagname) tagname = "*";
var all = root.getElementsByTagName(tagname);
if (!classname) return all;
var elements = [];
for (var i = 0; i < all.length; i++) {
var element = all[i];
if (isMember(element, classname) || isMember(element, classname2) || isMember(element, classname3) || isMember(element, classname4))
elements.push(element);
}
return elements;
}
function bnumClick(poemId, index) {
var msr1s = getElements("m1", "b2", "n", "l");
if (msr1s.length <= index) return;
var divId = 'bnumpanel' + String(index);
var existingDiv = document.getElementById(divId);
if (existingDiv != null) {
existingDiv.remove();
return;
}
var divParent = msr1s[index].className == "m1" ? msr1s[index].parentElement : msr1s[index];
var imgElementId = 'loadingimg-' + divId;
divParent.innerHTML = divParent.innerHTML + '<div class="bnumdiv" id="' + divId + '"><img id="' + imgElementId + '" src="/image/loading.gif" alt="بارگذاری"/></div>';
$.ajax({
type: "GET",
url: '?Handler=BNumPartial&poemId=' + String(poemId) + '&coupletIndex=' + String(index),
success: function (data) {
document.getElementById(imgElementId).style.display = "none";
var divId = 'bnumpanel' + String(index);
$(data).appendTo(document.getElementById(divId));
},
});
}
// Reveals the same beyt panel as clicking the (optional) couplet-number badge,
// but triggered by clicking the verse text itself - so it works even when
// line numbering is turned off. Reuses bnumClick's own element list so the
// index always matches what bnumClick itself expects, including for poems
// mixing .m1/.m2 couplets with single-line .b2/.n/.l verse types.
function verseTextClick(event) {
// don't double-fire when the click actually landed on the number badge
// or inside an already-revealed panel - both handle their own clicks
if (event.target.closest('.bnum') || event.target.closest('.bnumdiv')) return;
var verseHalf = event.target.closest('.m1, .m2, .b2, .n, .l');
if (!verseHalf) return;
// a real drag-select, double-click word-select, or long-press selection
// leaves text selected by the time click fires - let vaabd.js own that
var sel = window.getSelection();
if (sel && sel.toString().trim() !== '') return;
// .m2 (the second hemistich) isn't itself in bnumClick's element list -
// resolve it to the sibling .m1 within the same verse container
var indexTarget = verseHalf;
if (verseHalf.classList.contains('m2')) {
var container = verseHalf.parentElement;
indexTarget = container ? container.querySelector('.m1') : null;
if (!indexTarget) return;
}
var msr1s = getElements("m1", "b2", "n", "l");
var index = Array.prototype.indexOf.call(msr1s, indexTarget);
if (index === -1) return;
bnumClick(currentPoemId, index);
}
document.addEventListener('click', verseTextClick);
function coupletNumImage(bnum, color) {
let canvas = document.createElement('canvas');
canvas.width = 50;
canvas.height = 28;
let context = canvas.getContext('2d');
context.font = "1.5em 'Vazirmatn'";
context.fillStyle = color;
context.textAlign = "center";
context.textBaseline = "top";
context.imageSmoothingEnabled = true;
context.fillText(bnum, canvas.width / 2, 7);
return canvas.toDataURL();
}
function bshfarsinum(englishnum) {
var result = "";
for (var i = 0; i < englishnum.length; i++) {
result = result + String.fromCharCode(englishnum.charCodeAt(i) + 0x6C0);
}
return result;
}
function btshmr_internal(poemId) {
var bnum = getElements("bnum");
if (bnum.length != 0) {
lineNumbers = false;
for (var i = 0; i < bnum.length; ++i) {
bnum[i].remove();
}
var bnumdiv = getElements("bnumdiv");
for (var i = 0; i < bnumdiv.length; ++i) {
bnumdiv[i].remove();
}
}
else {
lineNumbers = true;
var msr1s = getElements("m1", "b2", "n", "l");
if (msr1s.length == 0) return true;
var j = 0;
var k = 1;
for (var i = 0; i < msr1s.length; ++i) {
if (msr1s[i].className != "b2") {
if (msr1s[i].className != "m1") {
msr1s[i].innerHTML = '<div class="bnum normalbnum" onclick="bnumClick(' + String(poemId) + ',' + String(i) + ');" id="bnum' + String(i + 1) + '"></div>' + msr1s[i].innerHTML;//no alt, so that when user copies text does not appear in the copied content
}
else {
msr1s[i].parentElement.innerHTML = '<div class="bnum normalbnum" onclick="bnumClick(' + String(poemId) + ',' + String(i) + ');" id="bnum' + String(i + 1) + '"></div>' + msr1s[i].parentElement.innerHTML;//no alt, so that when user copies text does not appear in the copied content
}
document.getElementById("bnum" + String(i + 1)).style.background = 'url(' + coupletNumImage(bshfarsinum(String(j + 1)), "red") + ')';
j++;
}
else {
msr1s[i].innerHTML = '<div class="bnum bandnum" onclick="bnumClick(' + String(poemId) + ',' + String(i) + ');" id="bnum' + String(i + 1) + '"></div>' + msr1s[i].innerHTML;
document.getElementById("bnum" + String(i + 1)).style.background = 'url(' + coupletNumImage("بند " + bshfarsinum(String(k)), "blue") + ')';
k++;
j = 0;
}
}
}
setCookie("lineNumbers", lineNumbers ? "true" : "false", 365);
var btnLineNumbers = document.getElementById("bnum-button");
if (lineNumbers) {
btnLineNumbers.classList.remove("color-disabled");
btnLineNumbers.classList.add("color-white");
}
else {
btnLineNumbers.classList.remove("color-white");;
btnLineNumbers.classList.add("color-disabled")
}
return true;
}
function btshmr(poemId) {
setTimeout(function () { btshmr_internal(poemId); }, 1);
}
function switchPlayerScrollLock() {
playerScrollLock = !playerScrollLock;
setCookie("playerScrollLock", playerScrollLock ? "true" : "false", 365);
if (playerScrollLock) {
$('#scroll-lock').text('🔒');
}
else {
$('#scroll-lock').text('🔓');
}
var stickyLockIcon = document.getElementById('sticky-scroll-lock-icon');
if (stickyLockIcon) {
stickyLockIcon.textContent = playerScrollLock ? 'lock' : 'lock_open';
}
if (playerScrollLock) {
const lockButtons = document.querySelectorAll('.recitation-scrollunlock');
lockButtons.forEach(function (lockButton) {
lockButton.classList.toggle('recitation-scrollunlock');
lockButton.classList.toggle('recitation-scrolllock');
});
alert('قفل متن روی خوانش فعال شد.');
}
else {
const lockButtons = document.querySelectorAll('.recitation-scrolllock');
lockButtons.forEach(function (lockButtons) {
lockButtons.classList.toggle('recitation-scrollunlock');
lockButtons.classList.toggle('recitation-scrolllock');
});
alert('قفل متن روی خوانش غیرفعال شد.');
}
}
function scrollToTargetAdjusted(element) {
var stickyNavbar = document.getElementsByClassName("sticky");
var headerOffset = stickyNavbar.length == 0 ? 0 : document.getElementById('main-navbar').clientHeight;
var elementPosition = element.getBoundingClientRect().top;
var offsetPosition = elementPosition + window.scrollY - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth"
});
}
function hilightverse(vnum, clr, sc, forceScroll) {
var root = document;
if (typeof root == "string") root = document.getElementById(root);
var all = root.getElementsByTagName("*");
var n = -1;
for (var i = 0; i < all.length; i++) {
var element = all[i];
if (isMember(element, "m1") || isMember(element, "m2") || isMember(element, "n") || isMember(element, "l")) {
n++;
if (n == vnum) {
element.style.color = clr;
if ($('#InlinePauseButton') != null) {
$('#InlinePauseButton').remove();
}
if ($('#InlineLockButton') != null) {
$('#InlineLockButton').remove();
}
if (sc == true) {
var btn = document.createElement("a");
btn.id = "InlinePauseButton";
btn.setAttribute('role', 'button');
btn.className = 'inlineanchor';
btn.onclick = function () {
if (currentAudio.paused) {
currentAudio.play();
$('#InlinePauseButtonImage').text('pause_circle_filled');
}
else {
currentAudio.pause();
$('#InlinePauseButtonImage').text('play_circle_filled');
}
};
element.lastChild.appendChild(btn);
var btnIcon = document.createElement("i");
btnIcon.className = 'inlinebutton';
btnIcon.innerText = 'pause_circle_filled';
btnIcon.id = "InlinePauseButtonImage"
btn.appendChild(btnIcon);
var btnLock = document.createElement("a");
btnLock.id = "InlineLockButton";
btnLock.setAttribute('role', 'button');
btnLock.className = 'inlineanchor';
btnLock.onclick = switchPlayerScrollLock;
element.lastChild.appendChild(btnLock);
var btnLockIcon = document.createElement("i");
btnLockIcon.className = 'inlinebutton';
btnLockIcon.innerText = 'lock';
btnLockIcon.id = "InlineLockButtonImage"
btnLock.appendChild(btnLockIcon);
if (forceScroll)
if (!!element && element.scrollIntoView) {
scrollToTargetAdjusted(element);
}
}
return true;
}
}
else
if (isMember(element, "b2")) {
var ptags = element.getElementsByTagName("p");
for (var p = 0; p < ptags.length; p++) {
if (ptags[p].parentElement.className == "bnum")
continue;
n++;
if (n == vnum) {
ptags[p].style.color = clr;
if ($('#InlinePauseButton') != null) {
$('#InlinePauseButton').remove();
}
if ($('#InlineLockButton') != null) {
$('#InlineLockButton').remove();
}
if (sc == true) {
var btn = document.createElement("a");
btn.id = "InlinePauseButton";
btn.setAttribute('role', 'button');
btn.className = 'inlineanchor';
btn.onclick = function () {
if (currentAudio.paused) {
currentAudio.play();
$('#InlinePauseButtonImage').text('pause_circle_filled');
}
else {
currentAudio.pause();
$('#InlinePauseButtonImage').text('play_circle_filled');
}
};
ptags[p].appendChild(btn);
var btnIcon = document.createElement("i");
btnIcon.className = 'inlinebutton';
btnIcon.innerText = 'pause_circle_filled';
btnIcon.id = "InlinePauseButtonImage"
btn.appendChild(btnIcon);
var btnLock = document.createElement("a");
btnLock.id = "InlineLockButton";
btnLock.setAttribute('role', 'button');
btnLock.className = 'inlineanchor';
btnLock.onclick = function () {
playerScrollLock = !playerScrollLock;
setCookie("playerScrollLock", playerScrollLock ? "true" : "false", 365);
if (playerScrollLock) {
$('#scroll-lock').text('🔒');
}
else {
$('#scroll-lock').text('🔓');
}
};
ptags[p].appendChild(btnLock);
var btnLockIcon = document.createElement("i");
btnLockIcon.className = 'inlinebutton';
btnLockIcon.innerText = 'lock';
btnLockIcon.id = "InlineLockButtonImage"
btnLock.appendChild(btnLockIcon);
if (forceScroll)
if (!!element && element.scrollIntoView) {
scrollToTargetAdjusted(element);
}
}
return true;
}
}
}
}
return false;
}
// ---------- sticky mini audio-player ----------
// Re-parents the actual playing <audio> element into a fixed bottom bar so its
// native seek/play/pause controls stay reachable while the user scrolls the
// poem text, instead of needing to scroll back to the recitation list. Moving
// an <audio> element via appendChild does not interrupt playback in modern
// browsers, so this is safe to do mid-play.
var stickyPlayerOriginalParent = null;
var stickyPlayerOriginalNextSibling = null;
var stickyPlayerDismissed = false;
var stickyPlayerLastRecitationId = null;
function showStickyPlayer(audioElement, narratorName, recitationId) {
if (recitationId !== stickyPlayerLastRecitationId) {
// a different recitation started - a manual dismissal of a previous
// track should not suppress the bar for a newly chosen one
stickyPlayerDismissed = false;
stickyPlayerLastRecitationId = recitationId;
}
if (stickyPlayerDismissed) return;
var bar = document.getElementById('sticky-audio-player');
var slot = document.getElementById('sticky-audio-slot');
if (!bar || !slot) return;
if (audioElement.parentElement !== slot) {
// if a DIFFERENT audio element is already sitting in the slot (e.g. the
// page-level recitation was playing and a couplet-level one just
// started), put it back where it came from first so the slot never
// ends up holding two <audio> elements at once
var existing = slot.firstElementChild;
if (existing && existing !== audioElement && stickyPlayerOriginalParent) {
if (stickyPlayerOriginalNextSibling) {
stickyPlayerOriginalParent.insertBefore(existing, stickyPlayerOriginalNextSibling);
} else {
stickyPlayerOriginalParent.appendChild(existing);
}
}
stickyPlayerOriginalParent = audioElement.parentElement;
stickyPlayerOriginalNextSibling = audioElement.nextSibling;
slot.appendChild(audioElement);
}
document.getElementById('sticky-audio-narrator').textContent = narratorName;
var lockIcon = document.getElementById('sticky-scroll-lock-icon');
if (lockIcon) {
lockIcon.textContent = playerScrollLock ? 'lock' : 'lock_open';
}
bar.style.display = 'flex';
}
function closeStickyPlayer() {
var slot = document.getElementById('sticky-audio-slot');
var bar = document.getElementById('sticky-audio-player');
if (!slot || !bar) return;
var audioElement = slot.firstElementChild;
if (audioElement && stickyPlayerOriginalParent) {
if (stickyPlayerOriginalNextSibling) {
stickyPlayerOriginalParent.insertBefore(audioElement, stickyPlayerOriginalNextSibling);
} else {
stickyPlayerOriginalParent.appendChild(audioElement);
}
}
bar.style.display = 'none';
stickyPlayerDismissed = true;
}
// Jumps to whichever verse currently matches the audio position, reusing the
// #InlinePauseButton marker that hilightverse() already keeps positioned at
// the currently-playing verse (recreated there every time the highlighted
// verse changes) - no need to independently track playback position.
function scrollToCurrentVerse() {
var marker = document.getElementById('InlinePauseButton');
if (!marker) return;
var verseElement = marker.closest('.m1, .m2, .n, .l, .b2');
if (verseElement && verseElement.scrollIntoView) {
scrollToTargetAdjusted(verseElement);
}
}
function fillnarrations(coupletIndex) {
if (typeof (narrators) == "undefined") {
var blockid = '#play-block-' + coupletIndex;
$(blockid).hide();
return;
}
if (narrators.length == 0) {
var blockid = '#play-block-' + coupletIndex;
$(blockid).hide();
return;
}
var comboId = '#narrators-' + coupletIndex;
for (var i = 0; i < narrators.length; i++) {
$(comboId).append(new Option(narrators[i].replace(/<\/?[^>]+(>|$)/g, "").replace('به خوانش ', '').replace('می‌خواهید شما بخوانید؟ اینجا را ببینید.', '').replace('(دریافت)', ''), i));
}
// default the combobox to whichever recitation is currently playing at the
// page level (currentAudio is set/updated by _AudioPlayerPartial.cshtml's
// 'play' listener, which fires for both page-level play buttons and
// playCouplet()'s own audioControl.play() call, so this stays in sync
// both ways without any extra wiring)
if (typeof currentAudio !== "undefined" && currentAudio != null) {
var currentAudioId = parseInt(currentAudio.id.replace('audio-', ''));
var currentIndex = recitaionIds.indexOf(currentAudioId);
if (currentIndex != -1) {
$(comboId).val(currentIndex);
}
}
}
function getVerseIndexFromCoupleIndex(coupletIndex) {
var tagname = "*";
var all = document.getElementsByTagName(tagname);
var msr1s = [];
for (var i = 0; i < all.length; i++) {
var element = all[i];
if (isMember(element, "m1") || isMember(element, "m2") || isMember(element, "b2") || isMember(element, "n") || isMember(element, "l"))
msr1s.push(element);
}
if ((msr1s.length - 1) < coupletIndex) return -1;
var cIndex = -1;
var vIndex = -1;
var bandNums = 0;
for (var i = 0; i < msr1s.length; ++i) {
if (msr1s[i].className != "m2") {
cIndex++;
}
if (cIndex == coupletIndex) {
vIndex = (i + bandNums);
break;
}
if (msr1s[i].className == "b2") {
if (msr1s[i].getElementsByTagName("p").length > 1)
bandNums++;
}
}
return vIndex;
}
function playCouplet(coupletIndex) {
var vIndex = getVerseIndexFromCoupleIndex(coupletIndex);
var comboId = '#narrators-' + coupletIndex;
var recitationIndex = parseInt($(comboId).find(":selected").val());
var recitationOrder = recitationIndex + 1;
if (audioxmlfiles.length > 0) {
$.ajax({
type: "GET",
url: audioxmlfiles[recitationIndex],
dataType: "xml",
success: function (xml) {
var nOneSecondBugFix = 2000;
$(xml).find('OneSecondBugFix').each(function () {
nOneSecondBugFix = parseInt($(xml).find('OneSecondBugFix').text());
});
var foundCouplet = false;
$(xml).find('SyncInfo').each(function () {
var v = parseInt($(this).find('VerseOrder').text())
if (v == vIndex) {
var verseStart = parseInt($(this).find('AudioMiliseconds').text()) / nOneSecondBugFix;
var audioControl = document.getElementById('audio-' + String(recitaionIds[recitationIndex]));
audioControl.play();
var buttonList = '#listen-' + coupletIndex;
$(buttonList).text('در حال دریافت خوانش ...');
setTimeout(function () {
audioControl.currentTime = verseStart;
$(buttonList).text('در حال خواندن');
}, 100);
foundCouplet = true;
return false;
}
});
if (!foundCouplet) {
alert('در این خوانش این خط خوانده نشده است.');
}
}
});
}
}
function editCouplet(poemId, coupletIndex, blockId) {
var vIndex = getVerseIndexFromCoupleIndex(coupletIndex);
location.href = '/User/Editor?id=' + poemId + '#' + blockId + '-' + String(vIndex + 1);
}
function switchBookmark(poemId, coupletIndex, divSuffix) {
$.ajax({
type: "GET",
url: '?Handler=PoemBookmarks&poemId=' + String(poemId),
error: function (err) {
alert('PoemBookmarks: ' + err.toString());
},
success: function (bookmarks) {
for (var i = 0; i < bookmarks.length; i++) {
var bookmark = bookmarks[i];
if (bookmark.coupletIndex == coupletIndex) {
if (bookmark.privateNote != null && bookmark.privateNote != '') {
if (!confirm('با حذف این نشان یادداشت آن نیز حذف خواهد شد. از این کار اطمینان دارید؟')) {
return;
}
}
break;
}
}
switchBookmarkInternal(poemId, coupletIndex, divSuffix);
},
});
}
function switchBookmarkInternal(poemId, coupletIndex, divSuffix) {
var iconElementId = coupletIndex < 0 ? 'bookmark-icon-comment-' + String(-coupletIndex) + divSuffix : 'bookmark-icon-' + String(coupletIndex);
var secondIconElementId = divSuffix != '' && coupletIndex < 0 ? 'bookmark-icon-comment-' + String(-coupletIndex) : null;
if (document.getElementById(iconElementId) != null) {
document.getElementById(iconElementId).innerHTML = 'star_half';
}
if (document.getElementById(secondIconElementId) != null) {
document.getElementById(secondIconElementId).innerHTML = 'star_half';
}
if (coupletIndex == 0) {
document.getElementById('bookmark-icon').innerHTML = 'star_half';
}
var url = '?handler=SwitchBookmark';
$.ajax({
type: "POST",
url: url,
data: {
poemId: poemId,
coupletIndex: coupletIndex
},
error: function (err) {
alert('SwitchBookmark: ' + err.toString())
},
success: function (bookmarkId) {
var isBookmarked = bookmarkId != '0';
if (isBookmarked == true) {
if (document.getElementById(iconElementId) != null) {
document.getElementById(iconElementId).innerHTML = 'star';
}
if (document.getElementById(secondIconElementId) != null) {
document.getElementById(secondIconElementId).innerHTML = 'star';
}
if (coupletIndex == 0) {
document.getElementById('bookmark').innerHTML = '<i class="noindent-info-button color-yellow" id="bookmark-icon">star</i>';
}
if (coupletIndex >= 0) {
tinymce.get('editNoteText').setContent('');
$("#editbookmarkId").val(bookmarkId);
document.getElementById('bookmark-note-dialog').style.display = 'block';
}
}
else {
if (document.getElementById(iconElementId) != null) {
document.getElementById(iconElementId).innerHTML = 'star_border';
}
if (document.getElementById(secondIconElementId) != null) {
document.getElementById(secondIconElementId).innerHTML = 'star_border';
}
if (coupletIndex == 0) {
document.getElementById('bookmark').innerHTML = '<i class="noindent-info-button color-white" id="bookmark-icon">star_border</i>';
}
}
},
});
}
function sortComments(poemId, sortOrder) {
var wrapper = document.getElementById('comments-list-wrapper');
if (wrapper == null) return;
$.ajax({
type: "GET",
url: '?handler=CommentsPartial&poemId=' + String(poemId) + '&sortOrder=' + sortOrder,
error: function (err) {
alert('خطا در بارگذاری حاشیه‌ها: ' + err.responseText);
},
success: function (html) {
wrapper.innerHTML = html;
paginateComments();
revealCommentFromUrlHash();
},
});
}
var COMMENTS_PAGE_SIZE = 15;
function paginateComments() {
var block = document.getElementById('comments-block');
if (block == null) return;
var existingButton = document.getElementById('comments-show-more');
if (existingButton != null) existingButton.remove();
// only direct children: root comments. Replies live nested inside each root's
// own div, so they're never touched by pagination - a full reply thread always
// stays together with its root.
var roots = Array.prototype.filter.call(block.children, function (el) {
return el.classList.contains('ganjoor-comment');
});
for (var i = 0; i < roots.length; i++) {
roots[i].style.display = '';
}
if (roots.length <= COMMENTS_PAGE_SIZE) return;
var shown = COMMENTS_PAGE_SIZE;
for (var i = shown; i < roots.length; i++) {
roots[i].style.display = 'none';
}
var button = document.createElement('a');
button.id = 'comments-show-more';
button.setAttribute('role', 'button');
button.className = 'cursor-pointer comments-link';
button.innerText = 'نمایش همهٔ حاشیه‌ها (' + toPersianNumber(roots.length - shown) + ' مورد دیگر)';
button.onclick = function () {
for (var i = shown; i < roots.length; i++) {
roots[i].style.display = '';
}
button.remove();
};
block.parentNode.insertBefore(button, block.nextSibling);
}
// Hashieha.cshtml, MyComments.cshtml, and copyCommentUrl() all link straight to
// #comment-<id>. Pagination can hide that target (or, if it's a reply, hide its whole
// parent thread, since only root comments get display:none) - the browser's native
// anchor-scroll runs before paginateComments() and silently fails against a hidden
// element, so this reveals the right root and scrolls to it explicitly afterwards.
function revealCommentFromUrlHash() {
if (!/^#comment-\d/.test(location.hash)) return;
var target = document.querySelector(location.hash);
if (target == null) return;
var block = document.getElementById('comments-block');
if (block != null) {
var el = target;
while (el != null && el.parentElement !== block) {
el = el.parentElement;
}
if (el != null) {
el.style.display = '';
}
}
setTimeout(function () {
target.scrollIntoView();
}, 0);
}
function rateComment(commentId, value, loggedIn, divSuffix) {
if (!loggedIn) {
alert('برای رأی دادن به حاشیه‌ها لازم است با نام کاربری خود وارد دیوان شوید.');
return;
}
var likeBtn = document.getElementById('like-btn-' + commentId + divSuffix);
var dislikeBtn = document.getElementById('dislike-btn-' + commentId + divSuffix);
// clicking the already-active button clears the rating instead of re-sending the same value
var newValue = value;
if (value == 1 && likeBtn.classList.contains('green-color')) newValue = 0;
if (value == -1 && dislikeBtn.classList.contains('red-color')) newValue = 0;
$.ajax({
type: "POST",
url: '?handler=RateComment',
data: {
id: commentId,
value: newValue
},
error: function (err) {
alert('خطا در ثبت رأی: ' + err.responseText);
},
success: function (result) {
_applyCommentRatingResult(commentId, divSuffix, result);
if (divSuffix != '') {
_applyCommentRatingResult(commentId, '', result);
}
},
});
}
function _applyCommentRatingResult(commentId, divSuffix, result) {
var likeBtn = document.getElementById('like-btn-' + commentId + divSuffix);
var dislikeBtn = document.getElementById('dislike-btn-' + commentId + divSuffix);
var likeCount = document.getElementById('like-count-' + commentId + divSuffix);
var dislikeCount = document.getElementById('dislike-count-' + commentId + divSuffix);
if (likeCount != null) likeCount.innerText = toPersianNumber(result.likeCount);
if (dislikeCount != null) dislikeCount.innerText = toPersianNumber(result.dislikeCount);
if (likeBtn != null) likeBtn.classList.remove('green-color');
if (dislikeBtn != null) dislikeBtn.classList.remove('red-color');
if (result.currentUserRatingValue == 1 && likeBtn != null) likeBtn.classList.add('green-color');
if (result.currentUserRatingValue == -1 && dislikeBtn != null) dislikeBtn.classList.add('red-color');
}
function checkIfBookmarked(poemId) {
if (document.getElementById('bookmark-icon') == null) return;
setTimeout(function () {
$.ajax({
type: "GET",
url: '?Handler=PoemBookmarks&poemId=' + String(poemId),
success: function (bookmarks) {
var isBookmarked = false;
for (var i = 0; i < bookmarks.length; i++) {
var bookmark = bookmarks[i];
if (bookmark.coupletIndex == 0) {
isBookmarked = true;
}
else if (bookmark.coupletIndex < 0) {
var commentBookmark = document.getElementById('bookmark-comment-' + (-bookmark.coupletIndex).toString());
if (commentBookmark != null) {
commentBookmark.innerHTML = '<i class="pageicons" id="bookmark-icon-comment-' + (-bookmark.coupletIndex).toString() + '">star</i>';
}
}
}
if (isBookmarked) {
document.getElementById('bookmark').innerHTML = '<i class="noindent-info-button color-yellow" id="bookmark-icon">star</i>';
}
else {
document.getElementById('bookmark').innerHTML = '<i class="noindent-info-button color-white" id="bookmark-icon">star_border</i>';
}
},
});
}, 1);
}
function savePrivateNote() {
$("#editnoteform").unbind('submit').bind('submit', function (e) {
e.preventDefault(); // avoid to execute the actual submit of the form.
var url = '?handler=BookmarkNote';
var bookmarkId = $("#editbookmarkId").val();
var originalHtml = $("textarea#editNoteText").val();
$.ajax({
type: "PUT",
url: url,
data: {
id: bookmarkId,
note: originalHtml
},
error: function (xhr) {
tryShowSanitizerErrorFromXhr(xhr, originalHtml);
},
success: function () {
document.getElementById('bookmark-note-dialog').style.display = 'none';
},
});
});
}
function checkWebShareSupport() {
if (navigator.share === undefined) {
document.getElementById('share_span').style.display = 'none';
}
}
function getPoemText() {
var all = $('#garticle').children();
var text = '';
for (var i = 0; i < all.length; i++) {
var element = all[i];
if (element.className == 'b' || element.className == 'b2' || element.className == 'n' || element.className == 'l') {
var elementSubs = $('#' + element.id).children();
for (var j = 0; j < elementSubs.length; j++) {
var elementSub = elementSubs[j];
if (elementSub.className == 'm1' || elementSub.className == 'm2' || elementSub.nodeName == 'P') {
if (text != '')
text += '\n';
text += elementSub.innerText;
}
if (elementSub.className == 'm2' || elementSub.className == 'b2') {
if (text != '')
text += '\n';
}
}
}
}
return text;
}
function getSummeriesText() {
var all = $('#summaries').children();
var text = '';
for (var i = 0; i < all.length; i++) {
var element = all[i];
if (element.className == 'coupletsummary') {
var elementSubs = $('#' + element.id).children();
for (var j = 0; j < elementSubs.length; j++) {
var elementSub = elementSubs[j];
if (elementSub.nodeName == 'BLOCKQUOTE') {
var quoteSubs = $('#' + elementSub.id).children();
for (var k = 0; k < quoteSubs.length; k++) {
if (quoteSubs[k].nodeName == 'P') {
if (text != '')
text += '\n';
text += quoteSubs[k].innerText.replace('#', '').trim();
}
}
}
else
if (elementSub.className == 'notice') {
var noticeSubs = $('#' + elementSub.id).children();
for (var k = 0; k < noticeSubs.length; k++) {
if (noticeSubs[k].nodeName == 'P') {
if (text != '') {
text += '\n\n';
}
text += noticeSubs[k].innerText;
text += '\n\n\n';
}
}
}
}
}
}
return text;
}
async function webSharePoem() {
var text = getPoemText();
var title = document.title;
var url = window.location.href;
try {
await navigator.share({ title, text, url });
} catch (error) {
alert('Error sharing: ' + error);
}
}
function copyPoemText() {
var text = getPoemText();
navigator.clipboard.writeText(text);
alert('متن در حافظه رونوشت شد.');
}
function copyPoemLink() {
var url = window.location.href;
if (url.indexOf('#') != -1) {
url = url.substring(0, url.indexOf('#'));
}
navigator.clipboard.writeText(url);
alert('نشانی در حافظه رونوشت شد.');
}
function copyCoupletUrl(coupletIndex) {
var url = window.location.href;
if (url.indexOf('#') != -1) {
url = url.substring(0, url.indexOf('#'));
}
url += ('#bn' + String(coupletIndex + 1));
navigator.clipboard.writeText(url);
alert('نشانی در حافظه رونوشت شد.');
}
function getCoupletText(coupletIndex) {
var all = $('#bn' + String(coupletIndex + 1)).children();
var text = '';
for (var i = 0; i < all.length; i++) {
var element = all[i];
if (element.className == 'm1' || element.className == 'm2' || element.nodeName == 'P') {
text += element.innerText;
text += '\n';
}
}
return text;
}
async function webShareCouplet(coupletIndex) {
var text = getCoupletText(coupletIndex);
var title = document.title;
var url = window.location.href + '#bn' + String(coupletIndex + 1);
try {
await navigator.share({ title, text, url });
} catch (error) {
alert('از همرسانی روی مرورگر جاری شما پشتیبانی نمی‌شود.')
}
}
function copyCoupletText(coupletIndex) {
var text = getCoupletText(coupletIndex);
navigator.clipboard.writeText(text);
alert('متن در حافظه رونوشت شد.');
}
function copySummeriesText() {
var text = getSummeriesText();
navigator.clipboard.writeText(text);
alert('متن در حافظه رونوشت شد.');
}
function wpopen(macagna) {
window.open(macagna, '_blank', 'width=600,height=700,scrollbars=yes,status=yes');
}
function postComment(coupletIndex, buttonSelector) {
var commentformSelector = '#commentform';
if (coupletIndex != '') {
commentformSelector += ('-' + coupletIndex);
}
$(commentformSelector).unbind('submit').bind('submit', function (e) {
e.preventDefault(); // avoid to execute the actual submit of the form.
$(buttonSelector).text('در حال درج حاشیه ...');
$(buttonSelector).prop("disabled", true);
$('#comment-error').remove();
var parent1 = $('#comments-block');
var parent2 = null;
if (coupletIndex != '') {
parent2 = $('#comments-block-' + coupletIndex);
}
var form = $(this);
var url = form.attr('action');
var originalHtml = form.find('textarea').val();
$.ajax({
type: "POST",
url: url,
data: form.serialize(), // serializes the form's elements.
error: function () {
$(buttonSelector).text('درج حاشیه');
$(buttonSelector).prop("disabled", false);
},
success: function (data) {
var $rendered = $(data);
var hadAnyError = $rendered.is('#comment-error');
var hadSanitizerError = checkAppendedHtmlForSanitizerError($rendered, originalHtml);
if (!hadSanitizerError) {
$rendered.appendTo(parent1);
if (parent2 != null)
$rendered.clone(true).appendTo(parent2);
}
$(buttonSelector).text('درج حاشیه');
$(buttonSelector).prop("disabled", false);
if (!hadAnyError) {
form[0].reset();
}
},
});
});
}
function replyComment(commentId, loggedIn, divSuffix) {
if (!loggedIn) {
alert('برای پاسخگویی لازم است با نام کاربری خود وارد دیوان شوید.');
return;
}
$("#refComment").html($('#comment-text-' + commentId + divSuffix).html());
$("#refCommentId").val(commentId);
$("#refCommentDivSuffix").val(divSuffix);
document.getElementById('id03').style.display = 'block';
}
function postReplyComment() {
parent = $('#comment-' + $("#refCommentId").val() + $("#refCommentDivSuffix").val());
$("#replycommentform").unbind('submit').bind('submit', function (e) {
e.preventDefault(); // avoid to execute the actual submit of the form.
$('#replycomment').text('در حال درج پاسخ به حاشیه ...');
$('#replycomment').prop("disabled", true);
$('#comment-error').remove();
var form = $(this);
var url = form.attr('action');
var originalHtml = form.find('textarea').val();
$.ajax({
type: "POST",
url: url,
data: form.serialize(), // serializes the form's elements.
error: function () {
$('#replycomment').text('درج پاسخ به حاشیه');
$('#replycomment').prop("disabled", false);
},
success: function (data) {
var $rendered = $(data);
var hadAnyError = $rendered.is('#comment-error');
var hadSanitizerError = checkAppendedHtmlForSanitizerError($rendered, originalHtml);
if (!hadAnyError) {
document.getElementById('id03').style.display = 'none';
$("#replycommentform")[0].reset();
}
if (!hadSanitizerError) {
$rendered.appendTo(parent);
}
$('#replycomment').prop("disabled", false);
},
});
});
}
function deleteMyComment(commentId, coupletIndex) {
if (!confirm('آیا از حذف این حاشیه اطمینان دارید؟'))
return;
var url = '?handler=MyComment';
$.ajax({
type: "DELETE",
url: url,
data: {
id: commentId
},
success: function () {
var commentBlockId = '#comment-' + commentId;
if ($(commentBlockId) != null) {
$(commentBlockId).remove();
}
if (coupletIndex != '-1') {
var bnumCommentBlockId = '#comment-' + commentId + "-" + coupletIndex;
if ($(bnumCommentBlockId) != null) {
$(bnumCommentBlockId).remove();
}
}
},
});
}
function deleteMistake(mistakeId) {
if (!confirm('آیا از حذف این اشکال اطمینان دارید؟'))
return;
var url = '?handler=Mistake';
$.ajax({
type: "DELETE",
url: url,
data: {
id: mistakeId
},
success: function () {
var commentBlockId = '#mistake-' + mistakeId;
if ($(commentBlockId) != null) {
$(commentBlockId).remove();
}
},
});
}
function editMistakeReason(mistakeId, reasonText) {
var edited = prompt('ویرایش', reasonText);
if (edited == null) return;
var url = '?handler=Mistake';
$.ajax({
type: "PUT",
url: url,
data: {
id: mistakeId,
reasonText: edited,
},
success: function () {
alert('انجام شد.');
},
});
}
function editMyComment(commentId, coupletIndex) {
var commentTextBlockId = '#comment-text-' + commentId;
tinymce.get('editCommentText').setContent($(commentTextBlockId).html());
$("#editCommentId").val(commentId);
$("#editCommentCoupletIndex").val(coupletIndex);
document.getElementById('id02').style.display = 'block';
}
function editComment() {
$("#editcommentform").unbind('submit').bind('submit', function (e) {
e.preventDefault(); // avoid to execute the actual submit of the form.
$('#editcomment').text('در حال ویرایش حاشیه ...');
$('#editcomment').prop("disabled", true);
var url = '?handler=MyComment';
var commentId = $("#editCommentId").val();
var coupletIndex = $("#editCommentCoupletIndex").val();
var originalHtml = $("textarea#editCommentText").val();
$.ajax({
type: "PUT",
url: url,
data: {
id: commentId,
comment: originalHtml
},
error: function (xhr) {
tryShowSanitizerErrorFromXhr(xhr, originalHtml);
$('#editcomment').text('ویرایش حاشیه');
$('#editcomment').prop("disabled", false);
},
success: function () {
document.getElementById('id02').style.display = 'none';
var commentTextBlockId = '#comment-text-' + commentId;
$(commentTextBlockId).html($("textarea#editCommentText").val());
if (coupletIndex != '-1') {
var bnumCommentBlockId = '#comment-text-' + commentId + "-" + coupletIndex;
if ($(bnumCommentBlockId) != null) {
$(bnumCommentBlockId).html($("textarea#editCommentText").val());
}
}
$('#editcomment').text('ویرایش حاشیه');
$('#editcomment').prop("disabled", false);
},
});
});
}
function copyCommentUrl(commentId) {
var url = window.location.href;
if (url.indexOf('#') != -1) {
url = url.substring(0, url.indexOf('#'));
}
url += ('#comment-' + String(commentId));
navigator.clipboard.writeText(url);
alert('نشانی در حافظه رونوشت شد.');
}
function onSelectedPoetChanged() {
if (document.getElementById('cat') != null) {
var id = document.getElementById('author').value;
if (id == 0) return;
$.ajax({
type: "GET",
url: '?handler=PoetInformation',
data: {
id: id
},
success: function (poet) {
if (poet == null)
return;
var select = document.getElementById("cat");
var length = select.options.length;
for (var i = length - 1; i >= 0; i--) {
select.options[i] = null;
}
option = document.createElement('option');
option.setAttribute('value', 0);
option.appendChild(document.createTextNode('در همهٔ بخشها'));
select.appendChild(option);
for (var i = 0; i < poet.cat.children.length; i++) {
option = document.createElement('option');
option.setAttribute('value', poet.cat.children[i].id);
option.appendChild(document.createTextNode(poet.cat.children[i].title));
select.appendChild(option);
}
},
});
}
}
function loadMoreRelatedPoems(poemId, skip, rhythm, rhymeLetters, poemFullUrl, sectionIndex) {
var loadButton = document.getElementById('load-more-button-' + String(sectionIndex));
if (loadButton != null) {
loadButton.remove();
}
var divParent = document.getElementById('load-more-related-' + String(sectionIndex));
var imgeId = 'load-more-related-loadingimg-' + String(sectionIndex);
divParent.innerHTML = divParent.innerHTML + '<img id="' + imgeId + '" src="/image/loading.gif" alt="بارگذاری "/>';
$.ajax({
type: "GET",
url: '?Handler=SimilarPoemsPartial&poemId=' + String(poemId) + '&skip=' + String(skip) + '&prosodyMetre=' + rhythm + '&rhymeLetters=' + rhymeLetters + '&poemFullUrl=' + poemFullUrl + '&sectionId=' + String(sectionIndex),
success: function (data) {
var imgElement = document.getElementById(imgeId);
imgElement.remove();
$(data).appendTo(document.getElementById('more-related-placeholder-' + String(sectionIndex)));
},
});
}
function loadMoreRelatedFromPoet(poetId, rhythm, rhymeLetters, skipPoemFullUrl1, skipPoemFullUrl2, sectionIndex) {
var buttonId = 'load-more-button-' + String(poetId) + '-' + String(sectionIndex);
var loadButton = document.getElementById(buttonId);
if (loadButton != null) {
loadButton.remove();
}
var divId = 'more-related-placeholder-' + String(poetId) + '-' + String(sectionIndex);
var divParent = document.getElementById(divId);
var imgeId = 'load-more-related-loadingimg-' + String(poetId) + '-' + String(sectionIndex);
divParent.innerHTML = divParent.innerHTML + '<img id="' + imgeId + '" src="/image/loading.gif" alt="بارگذاری "/>';
$.ajax({
type: "GET",
url: '?Handler=SimilarPoemsFromPoetPartial&poetId=' + String(poetId) + '&prosodyMetre=' + rhythm + '&rhymeLetters=' + rhymeLetters + '&skipPoemFullUrl1=' + skipPoemFullUrl1 + '&skipPoemFullUrl2=' + skipPoemFullUrl2,
success: function (data) {
var imgElement = document.getElementById(imgeId);
imgElement.remove();
$(data).appendTo(document.getElementById(divId));
},
});
}
function showAllRecitations() {
const hiddenRecitations = document.querySelectorAll('.hidden-recitation');
hiddenRecitations.forEach(function (recitation) {
recitation.className = 'audio-player';
});
const showRecitationsButtons = document.querySelectorAll('.load-all-recitations');
showRecitationsButtons.forEach(function (btn) {
btn.style.display = 'none';
});
}
function switch_section(sectionId, buttonId) {
if (document.getElementById(sectionId).style.display == 'none') {
document.getElementById(sectionId).style.display = 'block';
}
else {
document.getElementById(sectionId).style.display = 'none';
}
document.getElementById(buttonId).classList.toggle('expand_circle_down');
document.getElementById(buttonId).classList.toggle('collapse_circle_down');
}
function switchRecitationVote(recitationId) {
$.ajax({
type: "POST",
url: '?Handler=SwitchRecitationUpVote',
data: {
id: recitationId
},
success: function (upVote) {
if (upVote) {
document.getElementById('recitaion-' + String(recitationId)).classList.remove('recitation-novote');
document.getElementById('recitaion-' + String(recitationId)).classList.add('recitation-vote');
}
else {
document.getElementById('recitaion-' + String(recitationId)).classList.remove('recitation-vote');
document.getElementById('recitaion-' + String(recitationId)).classList.add('recitation-novote');
}
},
error: function (e) {
if (e.responseText == null)
alert('SwitchRecitationUpVote: ' + e.toString());
else
alert('SwitchRecitationUpVote: ' +e.responseText);
},
});
}
function MarkUserUpvotedRecitations(poemId) {
setTimeout(function () {
$.ajax({
type: "GET",
url: '?Handler=UserUpvotedRecitations&poemId=' + String(poemId),
success: function (result) {
for (var i = 0; i < result.length; i++) {
document.getElementById('recitaion-' + String(result[i])).classList.add('recitation-vote');
}
},
});
}, 1);
}
function persianizeNumerals(value) {
return value.replace(/0/gi, "۰").replace(/1/gi, "۱").replace(/2/gi, "۲")
.replace(/3/gi, "۳").replace(/4/gi, "۴").replace(/5/gi, "۵")
.replace(/6/gi, "۶").replace(/7/gi, "۷").replace(/8/gi, "۸")
.replace(/9/gi, "۹");
}
// Home page (Index.cshtml) only: the top search box (#s) used to just submit a full poem search
// (POST-free GET to /search) and the home page separately had its own "جستجوی سخنور" inline box
// (#findpoet, now removed) that filtered the poet list live as you typed. This merges that second
// behavior into the top box: typing filters poets live (same underlying match as onInlineSearch,
// against every ".poet" element's data-value), while submitting the form (Enter / the "بیاب" button)
// still does the original full poem search unchanged - so nothing about submission itself changes.
// homeBooksCache is filled once by initHomeBooksShelf() further below - the same catalog backing
// the "قفسهٔ کتابها" shelf is reused here so a search also matches book names, not just poets.
var homeBooksCache = [];
function buildHomeBookResultChip(book) {
var a = document.createElement('a');
a.className = 'home-book-result';
a.href = book.fullUrl || '#';
// Same 76x96 "portrait" box a poet result uses, just filled with the book's own hashed color
// and its name instead of a photo - see .home-book-result-cover in p8.css for the sizing.
var cover = document.createElement('span');
cover.className = 'home-book-result-cover';
cover.style.background = 'hsl(' + hashBookColorHue(book.id) + ', 42%, 32%)';
cover.textContent = book.name;
var caption = document.createElement('span');
caption.className = 'home-book-result-caption';
caption.textContent = book.poetName || '';
a.appendChild(cover);
a.appendChild(caption);
return a;
}
function onHomeSearchInput(value) {
var clearBtn = document.getElementById('home-search-clear');
var popularRow = document.getElementById('home-popular-poets-row');
var foundNode = document.getElementById('home-found-poets');
var hintNode = document.getElementById('home-search-hint');
if (!foundNode) return;
var hasValue = value.length > 0;
if (clearBtn) clearBtn.style.display = hasValue ? 'inline' : 'none';
foundNode.innerHTML = '';
if (!hasValue) {
foundNode.style.display = 'none';
if (popularRow) popularRow.style.display = '';
if (hintNode) hintNode.style.display = 'none';
return;
}
var replaced = persianizeNumerals(value);
// querySelectorAll (not getElementsByClassName) is required here: getElementsByClassName
// returns a LIVE collection, and the clones we append below also carry class="poet" and land
// earlier in document order than the originals (home-found-poets sits above the poet lists) -
// every append was reshuffling the live collection's indices so the same source element kept
// reappearing at the loop's current index forever, hanging the page. querySelectorAll takes a
// static snapshot up front, so appends during the loop can't affect it.
var poets = document.querySelectorAll('.poet');
// A poet who's one of the "سخنوران پرمخاطب" favorites is also listed again further down under
// their real century - that's the normal page structure, not a data error - so a name that
// matches both copies would otherwise get rendered twice. Dedupe by the poet's link, the one
// thing both copies share.
var seenHrefs = {};
for (var i = 0; i < poets.length; i++) {
var dataValue = poets[i].getAttribute('data-value');
if (dataValue == null || dataValue.indexOf(replaced) === -1) continue;
var link = poets[i].querySelector('a');
var href = link ? link.getAttribute('href') : null;
if (href != null) {
if (seenHrefs[href]) continue;
seenHrefs[href] = true;
}
foundNode.appendChild(poets[i].cloneNode(true));
}
for (var k = 0; k < homeBooksCache.length; k++) {
var book = homeBooksCache[k];
if (book.name && book.name.indexOf(replaced) !== -1) {
foundNode.appendChild(buildHomeBookResultChip(book));
}
}
var hasMatches = foundNode.childElementCount > 0;
foundNode.style.display = hasMatches ? '' : 'none';
// No matching poet: there's nothing useful to show in place of the favorites row, so put the
// page back the way it looked before the user started typing (same as clearing the box) rather
// than leaving an empty gap with the "press Enter" hint still showing over it.
if (popularRow) popularRow.style.display = hasMatches ? 'none' : '';
if (hintNode) hintNode.style.display = hasMatches ? 'block' : 'none';
}
function clearHomeSearch() {
var input = document.getElementById('s');
if (!input) return;
input.value = '';
onHomeSearchInput('');
input.focus();
}
// Home page (Index.cshtml) only: "قفسهٔ کتابها" - a horizontally scrollable shelf of book "spines",
// Grokipedia-homepage-style. Fetched once from this page's own "?Handler=BookCatalog" (which proxies
// RMuseum's api/ganjoor/book-catalog server-side) rather than calling that API directly from browser
// JS - same convention every other client-side data fetch on this site follows. There's no cover-art
// image for any book in the database, so each spine's color is a deterministic hash of its id instead
// of a real illustration - stable across visits/reloads (not reshuffled on every page load), while
// still giving the shelf the varied-color look a real bookshelf has.
var expandedBookSpine = null;
function hashBookColorHue(key) {
var str = String(key);
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = (hash * 31 + str.charCodeAt(i)) | 0;
}
return Math.abs(hash) % 360;
}
function collapseExpandedBookSpine() {
if (expandedBookSpine) {
expandedBookSpine.classList.remove('expanded');
expandedBookSpine = null;
}
}
function buildBookSpine(book) {
var spine = document.createElement('div');
spine.className = 'book-spine';
spine.tabIndex = 0;
spine.setAttribute('role', 'button');
spine.setAttribute('aria-label', book.name + (book.poetName ? ' - ' + book.poetName : ''));
var hue = hashBookColorHue(book.id);
spine.style.background =
'linear-gradient(90deg, rgba(0,0,0,.22), transparent 12%, transparent 88%, rgba(255,255,255,.12)), ' +
'hsl(' + hue + ', 42%, 30%)';
// The compact (spine) face: book name on top, poet name below (smaller) - both laid out as a
// normal horizontal line of text first, so Persian letter-joining/shaping happens correctly,
// then rotated 90deg as a whole block. True CSS vertical writing-mode (the "right" way to do
// this for Latin text, which is how Grokipedia's own spines work) breaks cursive Arabic/Persian
// script - it uprights each letter separately instead of rotating the shaped line - so rotating
// the finished line is the only way to get this to read correctly.
var compact = document.createElement('div');
compact.className = 'spine-compact';
var title = document.createElement('span');
title.className = 'spine-title';
title.textContent = book.name;
var poet = document.createElement('span');
poet.className = 'spine-poet';
poet.textContent = book.poetName || '';
compact.appendChild(title);
compact.appendChild(poet);
// The expanded (cover) face: shown once this spine is clicked - see openOrNavigateBookSpine().
var cover = document.createElement('div');
cover.className = 'spine-cover';
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'spine-cover-close';
closeBtn.setAttribute('aria-label', 'بستن');
closeBtn.textContent = '×';
closeBtn.addEventListener('click', function (e) {
e.stopPropagation();
collapseExpandedBookSpine();
});
var coverTitle = document.createElement('div');
coverTitle.className = 'spine-cover-title';
coverTitle.textContent = book.name;
var coverPoet = document.createElement('div');
coverPoet.className = 'spine-cover-poet';
coverPoet.textContent = book.poetName || '';
cover.appendChild(closeBtn);
cover.appendChild(coverTitle);
cover.appendChild(coverPoet);
spine.appendChild(compact);
spine.appendChild(cover);
function openOrNavigateBookSpine() {
if (spine.classList.contains('expanded')) {
if (book.fullUrl) window.location.href = book.fullUrl;
return;
}
// Only one spine open at a time - collapsing the previous one first is what makes a growing
// spine read as "picking a different book" rather than stacking expanded covers.
collapseExpandedBookSpine();
spine.classList.add('expanded');
expandedBookSpine = spine;
// The spine's own width grows a lot (56px -> 170px) - without this, it can end up partly
// scrolled out of view, which looks like nothing happened.
spine.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
}
spine.addEventListener('click', openOrNavigateBookSpine);
spine.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openOrNavigateBookSpine();
}
});
return spine;
}
// Called by the "<"/">" handles next to the shelf (see Index.cshtml). amount is signed in the
// ordinary "positive = move the view right, negative = move it left" sense - evergreen browsers
// (Chrome/Firefox/Safari) all follow the spec's "negative" RTL scrollLeft model, where scrollLeft
// is 0 at the right/start edge and goes negative toward the left/end edge, so a plain scrollBy
// without any RTL sign-flip already moves the view the way its own delta's sign says.
function scrollBooksShelf(amount) {
var wrap = document.getElementById('books-shelf-wrap');
if (!wrap) return;
wrap.scrollBy({ left: amount, behavior: 'smooth' });
}
function initHomeBooksShelf() {
var shelf = document.getElementById('books-shelf');
if (!shelf) return;
$.ajax({
type: 'GET',
url: '?Handler=BookCatalog',
success: function (books) {
if (!books || books.length === 0) return;
homeBooksCache = books;
var frag = document.createDocumentFragment();
for (var i = 0; i < books.length; i++) {
frag.appendChild(buildBookSpine(books[i]));
}
shelf.appendChild(frag);
}
});
}
function onInlineSearch(value, resultBlockId, itemsClass) {
const foundPoetsNode = document.getElementById(resultBlockId);
foundPoetsNode.innerHTML = '';
if (value.length > 0) {
let replaced = persianizeNumerals(value);
var poets = document.getElementsByClassName(itemsClass);
var foundOnes = [];
for (var i = 0; i < poets.length; i++) {
var dataValue = poets[i].getAttribute("data-value");
if (dataValue != null) {
if (dataValue.indexOf(replaced) != -1) {
foundOnes.push(poets[i]);
}
}
}
for (var i = 0; i < foundOnes.length; i++) {
var clonedPoet = foundOnes[i].cloneNode(true);
foundPoetsNode.appendChild(clonedPoet);
}
}
}
function AddToMyHistory(poemId) {
setTimeout(function () {
$.ajax({
type: "POST",
url: '?Handler=AddToMyHistory&poemId=' + String(poemId),
success: function (result) {
console.log(result);
$('#track-record').html('<small>' + result + ' <a href="/User/History">*</a></small>');
},
});
}, 1);
}
function CheckIfHasNotificationsForHomePage() {
setTimeout(function () {
$.ajax({
type: "GET",
url: '?Handler=CheckIfHasNotifications',
success: function (result) {
if (result != '') {
document.getElementById('notification-badge').classList.toggle('display-none');
document.getElementById('notification-badge').classList.toggle('visible-notification-badge');
}
},
});
}, 1);
}
function doSearchInRhythmsCombo(selectSearchId, rhythmnewId) {
var value = document.getElementById(selectSearchId).value;
var options = document.getElementById(rhythmnewId).options
for (var i = 0; i < options.length; i++) {
if (options[i].text.indexOf(value) != -1) {
document.getElementById(rhythmnewId).value = options[i].value;
break;
}
}
}
function resetRhythm(rhythmnewId) {
document.getElementById(rhythmnewId).value = 'null';
}
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
let expires = "expires=" + d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function w3_open() {
if (document.getElementById("mySidebar").style.display == "none") {
document.getElementById("mySidebar").style.display = "block";
}
else {
document.getElementById("mySidebar").style.display = "none";
}
}
function w3_close() {
document.getElementById("mySidebar").style.display = "none";
}
function w3_close_showHelp() {
document.getElementById("mySidebar").style.display = "none";
document.getElementById('navbarhelp').style.display = 'block'
}
function viewLocation(lt, lg) {
link = document.createElement("a")
link.href = 'https://maps.google.com/?q=' + lt + ',' + lg + '&ll=' + lt + ',' + lg + '&z=3';
link.target = "_blank"
link.click()
}
function loadCatRecitations(catId) {
var loadButton = document.getElementById("load-cat-recitations");
if (loadButton == null) {
return;
}
loadButton.remove();
var divParent = document.getElementById('recitations-section');
var imgElementId = 'loadingimg';
divParent.innerHTML = divParent.innerHTML + '<div class="bnumdiv" id="remove-this"><img id="' + imgElementId + '" src="/image/loading.gif" alt="بارگذاری"/></div>';
$.ajax({
type: "GET",
url: '?Handler=CategoryRecitations&catId=' + String(catId),
success: function (data) {
document.getElementById("remove-this").remove();
$(data).appendTo(divParent);
},
});
}
// on-demand "شخصیت‌ها" tab on a poet/category page - loads the category-scoped person/relation
// graph (see _PersonGraphPartial.cshtml / OnGetPersonGraphAsync) and hands its JSON payload to the
// same peoplegraph.js force-directed renderer used by the PeopleExplorer.open() modal, with element ids prefixed
// "pg-cat-" so it doesn't collide with anything else on the page
function loadPersonGraph(catId) {
var loadButton = document.getElementById("load-persongraph");
if (loadButton == null) {
return;
}
loadButton.remove();
var divParent = document.getElementById('persongraph-placeholder');
var imgElementId = 'loadingpersongraphimg';
divParent.innerHTML = divParent.innerHTML + '<div class="bnumdiv" id="remove-this-persongraph"><img id="' + imgElementId + '" src="/image/loading.gif" alt="بارگذاری"/></div>';
$.ajax({
type: "GET",
url: '?Handler=PersonGraph&catId=' + String(catId),
error: function () {
if (document.getElementById("remove-this-persongraph") != null) {
document.getElementById("remove-this-persongraph").remove();
}
},
success: function (data) {
if (document.getElementById("remove-this-persongraph") != null) {
document.getElementById("remove-this-persongraph").remove();
}
$(data).appendTo(divParent);
var dataElement = document.getElementById('pg-cat-data');
if (dataElement != null) {
var graphData = JSON.parse(dataElement.textContent);
GanjoorPeopleGraph.render({
containerId: 'pg-cat-container',
svgId: 'pg-cat-svg',
tableBodyId: 'pg-cat-table-body',
legendId: 'pg-cat-legend',
searchInputId: 'pg-cat-search',
resetButtonId: 'pg-cat-reset',
data: graphData
});
}
},
});
}
// https://stackoverflow.com/questions/56300132/how-to-override-css-prefers-color-scheme-setting
// Return the system level color scheme, but if something's in local storage, return that
// Unless the system scheme matches the the stored scheme, in which case... remove from local storage
function getPreferredColorScheme() {
let systemScheme = 'light';
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
systemScheme = 'dark';
}
let chosenScheme = systemScheme;
if (localStorage.getItem("scheme")) {
chosenScheme = localStorage.getItem("scheme");
}
if (systemScheme === chosenScheme) {
localStorage.removeItem("scheme");
}
return chosenScheme;
}
// Write chosen color scheme to local storage
// Unless the system scheme matches the the stored scheme, in which case... remove from local storage
function savePreferredColorScheme(scheme) {
let systemScheme = 'light';
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
systemScheme = 'dark';
}
if (systemScheme === scheme) {
localStorage.removeItem("scheme");
}
else {
localStorage.setItem("scheme", scheme);
}
}
// Get the current scheme, and apply the opposite
function toggleColorScheme() {
let newScheme = "light";
let scheme = getPreferredColorScheme();
if (scheme === "light") {
newScheme = "dark";
}
applyPreferredColorScheme(newScheme);
savePreferredColorScheme(newScheme);
}
// Apply the chosen color scheme by traversing stylesheet rules, and applying a medium.
function applyPreferredColorScheme(scheme) {
for (var s = 0; s < document.styleSheets.length; s++) {
var sheet = document.styleSheets[s];
var rules;
try {
rules = sheet.cssRules;
} catch {
continue;
}
for (var i = 0; i < rules.length; i++) {
var rule = rules[i];
if (rule && rule.media && rule.media.mediaText.includes("prefers-color-scheme")) {
switch (scheme) {
case "light":
rule.media.appendMedium("original-prefers-color-scheme");
if (rule.media.mediaText.includes("light")) rule.media.deleteMedium("(prefers-color-scheme: light)");
if (rule.media.mediaText.includes("dark")) rule.media.deleteMedium("(prefers-color-scheme: dark)");
break;
case "dark":
rule.media.appendMedium("(prefers-color-scheme: light)");
rule.media.appendMedium("(prefers-color-scheme: dark)");
if (rule.media.mediaText.includes("original")) rule.media.deleteMedium("original-prefers-color-scheme");
break;
default:
rule.media.appendMedium("(prefers-color-scheme: dark)");
if (rule.media.mediaText.includes("light")) rule.media.deleteMedium("(prefers-color-scheme: light)");
if (rule.media.mediaText.includes("original")) rule.media.deleteMedium("original-prefers-color-scheme");
break;
}
}
}
}
// Change the toggle button to be the opposite of the current scheme
if (scheme === "dark") {
if (document.getElementById("icon-sun") != null) {
document.getElementById("icon-sun").style.display = 'inline';
document.getElementById("icon-moon").style.display = 'none';
}
} else {
if (document.getElementById("icon-sun") != null) {
document.getElementById("icon-moon").style.display = 'inline';
document.getElementById("icon-sun").style.display = 'none';
}
}
}
function countAndSortCharacters(inputString) {
// Initialize an empty map to store character counts
const charCountMap = new Map();
// Iterate through each character in the input string
for (const char of inputString) {
if (char == ' ') continue;
// Check if the character is already in the map
if (charCountMap.has(char)) {
// If yes, increment the count
charCountMap.set(char, charCountMap.get(char) + 1);
} else {
// If not, add the character to the map with a count of 1
charCountMap.set(char, 1);
}
}
// Sort the map by character counts in descending order
const sortedCharCount = new Map([...charCountMap.entries()].sort((a, b) => b[1] - a[1]));
// Return the sorted map
return sortedCharCount;
}
function countAndDisplayCharacters(inputString, resultTableId, sourceStringId) {
const result = countAndSortCharacters(inputString);
const container = document.getElementById(resultTableId);
let highlightedCharDiv = null; // To keep track of the previously clicked charDiv
// Populate the row with cells for each character and its frequency
var first = true;
result.forEach((count, char) => {
const charDiv = document.createElement('div');
charDiv.textContent = `${char}: ${persianizeNumerals(count.toString())}`;
charDiv.className = 'charCell';
// Add a click event listener to highlight the character in the source string
charDiv.addEventListener('click', (event) => {
if (event.target.classList.contains('charCell')) {
// Roll back the color of the previously clicked charDiv
if (highlightedCharDiv) {
highlightedCharDiv.classList.remove('background-red');
}
const sourceStringDiv = document.getElementById(sourceStringId);
const highlightedString = highlightCharacter(inputString, char, sourceStringDiv.innerHTML);
sourceStringDiv.innerHTML = highlightedString;
// Toggle the color of the clicked charDiv
charDiv.classList.toggle('background-red');
highlightedCharDiv = charDiv; // Update the reference to the clicked charDiv
}
});
// Highlight the first character by default
if (first) {
first = false;
charDiv.classList.add('background-red');
highlightedCharDiv = charDiv;
const sourceStringDiv = document.getElementById(sourceStringId);
const highlightedString = highlightCharacter(inputString, char, sourceStringDiv.innerHTML);
sourceStringDiv.innerHTML = highlightedString;
}
// Append the div to the container
container.appendChild(charDiv);
});
}
function highlightCharacter(previousHighlightedString, charToHighlight) {
// Escape special characters in the regex pattern
const escapedChar = charToHighlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Create a regular expression to match the specified character
const regex = new RegExp(escapedChar, 'g');
// Remove previous highlighting
const unhighlightedString = previousHighlightedString.replace(/<span style="color: red;">|<\/span>/g, '');
// Replace the matched character with the HTML code for red color
const highlightedString = unhighlightedString.replace(regex, `<span style="color: red;">$&</span>`);
return highlightedString;
}
function inlineHighlight() {
var searchTerm = document.getElementById('inline-search').value;
if (searchTerm.trim().length == 0) {
inlineUnHighlight();
return;
}
var context = document.getElementById('garticle');
$(context).unmark({
done: function () {
$(context).mark(searchTerm, {
ignoreJoiners: true,
separateWordSearch: false,
ignorePunctuation: 'ًٌٍَُّ.،!؟ٔ:؛;*)([]«»ْ'.split(''),
"exclude": [
"#poet-image *",
"#page-hierarchy *",
"#utils-navbar *",
],
done: function (markCount) {
inlineSearchResultIndex = 0;
if (markCount == 0) {
document.getElementById('inline-search').classList.add("background-red");
}
else {
document.getElementById('inline-search').classList.remove("background-red");
inlineSearchResults = $("#garticle").find("mark");
jumpToInlineSearchResult();
}
$('#inline-search-matches').text(persianizeNumerals((inlineSearchResultIndex + 1).toString()) +' از '+ persianizeNumerals(markCount.toString()));
}
});
}
});
}
function inlineUnHighlight() {
document.getElementById('inline-search').value = '';
document.getElementById('fake-inline-div').value = '';
document.getElementById('inline-search').classList.remove("background-red");
$('#inline-search-matches').text('');
var context = document.getElementById('garticle');
$(context).unmark();
document.getElementById('fake-inline-div').style.display = 'block';
document.getElementById('main-inline-div').style.display = 'none';
document.getElementById('fake-inline-search').value = '';
document.getElementById('fake-inline-search').focus();
}
function displayMainInlineSearch() {
document.getElementById('fake-inline-div').style.display = 'none';
document.getElementById('main-inline-div').style.display = 'block';
var txt = document.getElementById('fake-inline-search').value;
document.getElementById('inline-search').value = txt;
document.getElementById('inline-search').setSelectionRange(txt.length, txt.length);
document.getElementById('inline-search').focus();
}
function jumpToInlineSearchResult() {
if (inlineSearchResults.length) {
var position,
$current = inlineSearchResults.eq(inlineSearchResultIndex);
inlineSearchResults.removeClass(currentClass);
if ($current.length) {
$current.addClass(currentClass);
position = $current.offset().top - offsetTop;
window.scrollTo(0, position);
$('#inline-search-matches').text(persianizeNumerals((inlineSearchResultIndex + 1).toString()) + ' از ' + persianizeNumerals(inlineSearchResults.length.toString()));
}
}
}
function nextInlineSearchResult() {
if (inlineSearchResultIndex < (inlineSearchResults.length - 1)) {
inlineSearchResultIndex++;
}
jumpToInlineSearchResult();
}
function prevInlineSearchResult() {
if (inlineSearchResultIndex > 0) {
inlineSearchResultIndex--;
}
jumpToInlineSearchResult();
}
function loadMoreQuotedForRelatedPoem(quoteRecordId, poemId, relatedPoemId, poetImageUrl, poetNickName, canEdit) {
var buttonId = 'load-more-quoted-button-' + quoteRecordId;
var loadButton = document.getElementById(buttonId);
if (loadButton != null) {
loadButton.remove();
}
var divId = 'more-quoted-placeholder-' + quoteRecordId;
var divParent = document.getElementById(divId);
var imgeId = 'load-more-quoted-loadingimg-' + quoteRecordId;
divParent.innerHTML = divParent.innerHTML + '<img id="' + imgeId + '" src="/image/loading.gif" alt="بارگذاری "/>';
$.ajax({
type: "GET",
url: '?Handler=MoreQuotedPoemsForRelatedPoemPartial',
data: {
poemId: poemId,
relatedPoemId: relatedPoemId,
poetImageUrl: poetImageUrl,
poetNickName: poetNickName,
canEdit: canEdit
},
success: function (data) {
var imgElement = document.getElementById(imgeId);
imgElement.remove();
$(data).appendTo(document.getElementById(divId));
},
});
}
function loadMoreQuotedPoems(poemId, skip, poetImageUrl, poetNickName, canEdit) {
var loadButton = document.getElementById('load-more-quoted');
if (loadButton != null) {
loadButton.remove();
}
var divParent = document.getElementById('more-quoted-placeholder');
var imgeId = 'load-more-quoted-loadingimg';
divParent.innerHTML = divParent.innerHTML + '<img id="' + imgeId + '" src="/image/loading.gif" alt="بارگذاری "/>';
$.ajax({
type: "GET",
url: '?Handler=MoreQuotedPoemsPartial',
data: {
poemId: poemId,
skip: skip,
poetImageUrl: poetImageUrl,
poetNickName: poetNickName,
canEdit: canEdit
},
success: function (data) {
var imgElement = document.getElementById(imgeId);
imgElement.remove();
$(data).appendTo(document.getElementById('more-quoted-placeholder'));
},
});
}
function markAsTextOriginal(bookId, categoryId, bookName, catName) {
if (!confirm('آیا ' + bookName + ' منبع کاغذی بخش ' + catName + ' است؟'))
return;
var url = '?handler=MarkAsTextOriginal';
$.ajax({
type: "POST",
url: url,
data: {
bookId: bookId,
categoryId: categoryId
},
success: function () {
alert('فرایند کار شروع شد.');
},
});
}
function deleteRelatedImage(relatedImageType, linkId, altText) {
if (!confirm('آیا از قطع ارتباط ' + altText + ' با این بخش اطمینان دارید؟'))
return;
var removeItemLink = false;
if (relatedImageType == 0) {
if (!confirm('لینک از طرف گنجینه هم قطع شود؟')) {
removeItemLink = true;
}
}
var url = '?handler=RelatedImageLink';
$.ajax({
type: "DELETE",
url: url,
data: {
relatedImageType: relatedImageType,
linkId: linkId,
removeItemLink: removeItemLink
},
error: function (err) {
alert('RelatedImageLink: ' + err.toString());
},
success: function () {
alert('انجام شد.');
location.reload();
},
});
}
function reloadWordCounts(catId, poetId, remStopWords) {
var divParent = document.getElementById('wordcounts-placeholder');
divParent.innerHTML = '<div id="wordcounts-placeholder"><div>';
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 + '<div class="bnumdiv" id="remove-this-wordcounts"><img id="' + imgElementId + '" src="/image/loading.gif" alt="بارگذاری"/></div>';
$.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 = '<div class="bnumdiv" id="remove-this-wordcounts"><img id="' + imgElementId + '" src="/image/loading.gif" alt="بارگذاری"/></div>';
$.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 + '<div class="loadingcontainer" id="remove-this-wordcounts"><img id="' + imgElementId + '" src="/image/loading.gif" alt="بارگذاری"/></div>';
$.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 + '<div class="bnumdiv" id="remove-this-wordcounts"><img id="' + imgElementId + '" src="/image/loading.gif" alt="بارگذاری"/></div>';
$.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 = '<div id="wordcounts-placeholder"><div>'
$(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 <table> 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 = '<div class="bnumdiv" id="remove-days-placeholder"><img src="/image/loading.gif" alt="بارگذاری"/></div>';
$.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 = '<div class="bnumdiv" id="remove-days-placeholder"><img src="/image/loading.gif" alt="بارگذاری"/></div>';
$.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 = '<div class="bnumdiv" id="remove-days-placeholder"><img src="/image/loading.gif" alt="بارگذاری"/></div>';
$.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 = '<div class="bnumdiv" id="remove-this-topvisits"><img id="' + imgElementId + '" src="/image/loading.gif" alt="بارگذاری"/></div>';
$.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 = '<div class="bnumdiv" id="remove-this-7daysvisits"><img id="' + imgElementId + '" src="/image/loading.gif" alt="بارگذاری"/></div>';
$.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 <input> into a "search as you type" picker over a small
// in-memory location catalog ({id, name, latitude, longitude}[]), instead of a
// long <select> 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 <input> the user types a location name into
// hiddenInput - an existing/hidden <input> 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 <select>'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();
}
});
}