// --- CSRF (antiforgery) token for AJAX calls -------------------------------------------- // Every POST/PUT/DELETE handler in the app validates an antiforgery token by default. A real //
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 involved, so they need to attach the token // themselves. The token value is rendered once per page as (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 + '
بارگذاری
'; $.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 = '
' + msr1s[i].innerHTML;//no alt, so that when user copies text does not appear in the copied content } else { msr1s[i].parentElement.innerHTML = '
' + 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 = '
' + 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