- 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>
3019 lines
117 KiB
JavaScript
3019 lines
117 KiB
JavaScript
// --- 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 + '§ionId=' + 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();
|
||
}
|
||
});
|
||
}
|