MediaWiki:Common.js: Difference between revisions
No edit summary |
No edit summary |
||
| Line 160: | Line 160: | ||
if (p.closest('.gr-controls')) return; | if (p.closest('.gr-controls')) return; | ||
if (p.closest('.mw-editsection')) return; | if (p.closest('.mw-editsection')) return; | ||
// Note markers are [1], [2] … — machinery, not text, and | |||
// the numbers would be transliterated along with the page. | |||
if (p.closest('.gr-note-marker')) return; | |||
} | } | ||
var orig = node.textContent; | var orig = node.textContent; | ||
| Line 794: | Line 797: | ||
var p = node.parentNode; | var p = node.parentNode; | ||
if (!p || (p.hasAttribute && p.hasAttribute('data-deva'))) return; | if (!p || (p.hasAttribute && p.hasAttribute('data-deva'))) return; | ||
if (p.closest && (p.closest('.gr-controls') || p.closest('.mw-editsection'))) return; | if (p.closest && (p.closest('.gr-controls') || p.closest('.mw-editsection') | ||
|| p.closest('.gr-note-marker'))) return; | |||
var orig = node.textContent; | var orig = node.textContent; | ||
if (!orig.trim()) return; | if (!orig.trim()) return; | ||
| Line 2,517: | Line 2,521: | ||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', decorate); | if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', decorate); | ||
else decorate(); | else decorate(); | ||
}()); | |||
// ── Footnote markers and their cards ──────────────────────────────── | |||
/* QuickEdit already stores footnotes: it wraps the annotated words in | |||
<span class="gra-qe-footnote-highlight" data-gra-id="…"> and appends a | |||
<div class="gra-qe-footnotes"><ol> at the foot of the document, one | |||
<li data-gra-id="…"> per note carrying the quoted words and the note. | |||
That is the whole apparatus, but reading it meant scrolling to the end of | |||
a long document and scrolling back. This turns the same data into what | |||
Setutila shows: a small superscript after the annotated words, and a card | |||
at the point of reading. Nothing is stored differently and QuickEdit is | |||
untouched — the list is still written, still saved, and still printed; it | |||
is only taken off the screen, where the markers replace it. | |||
The note text is transliterated with the rest of the page: the card's body | |||
is tagged data-deva and announced through gr-new-content, which the script | |||
switcher already listens for. */ | |||
(function () { | |||
'use strict'; | |||
var HL = '.gra-qe-footnote-highlight'; | |||
var LIST = '.gra-qe-footnotes'; | |||
var TYPES = { // data-gra-type on the <li>, if set | |||
pathantara: 'पाठान्तरम्', | |||
source: 'मूलम्', | |||
note: 'टिप्पणी' | |||
}; | |||
var card = null, openFor = null; | |||
function notesFromList() { | |||
var out = {}; | |||
document.querySelectorAll(LIST + ' .gra-qe-footnote-item').forEach(function (li) { | |||
var id = li.getAttribute('data-gra-id'); | |||
if (!id) return; | |||
var textEl = li.querySelector('.gra-qe-footnote-text'); | |||
out[id] = { | |||
text: (textEl ? textEl.textContent : li.textContent).trim(), | |||
quote: (li.querySelector('.gra-qe-footnote-quote') || {}).textContent || '', | |||
type: li.getAttribute('data-gra-type') || 'note' | |||
}; | |||
}); | |||
return out; | |||
} | |||
function buildCard() { | |||
if (card) return card; | |||
card = document.createElement('div'); | |||
card.className = 'gr-note-card'; | |||
card.setAttribute('role', 'dialog'); | |||
card.hidden = true; | |||
card.innerHTML = | |||
'<div class="gr-note-card-head">' + | |||
'<span class="gr-note-card-kind"></span>' + | |||
'<button type="button" class="gr-note-card-close" aria-label="Close">×</button>' + | |||
'</div><div class="gr-note-card-body"></div>'; | |||
card.querySelector('.gr-note-card-close').addEventListener('click', close); | |||
document.body.appendChild(card); | |||
return card; | |||
} | |||
function close() { | |||
if (!card || card.hidden) return; | |||
card.hidden = true; | |||
if (openFor) { | |||
openFor.setAttribute('aria-expanded', 'false'); | |||
openFor.classList.remove('gr-note-marker-open'); | |||
} | |||
openFor = null; | |||
} | |||
// Anchored under the marker, nudged back inside the viewport rather than | |||
// flipped: these sit in running text, and a card that jumps above the line | |||
// is harder to connect to the word it belongs to than one that shifts left. | |||
function place(marker) { | |||
var r = marker.getBoundingClientRect(); | |||
var w = card.offsetWidth, h = card.offsetHeight; | |||
var pad = 10; | |||
var left = r.left + r.width / 2 - w / 2; | |||
left = Math.max(pad, Math.min(left, window.innerWidth - w - pad)); | |||
var top = r.bottom + 8; | |||
if (top + h > window.innerHeight - pad && r.top - h - 8 > pad) top = r.top - h - 8; | |||
card.style.left = Math.round(left) + 'px'; | |||
card.style.top = Math.round(top) + 'px'; | |||
} | |||
function open(marker, note, num) { | |||
buildCard(); | |||
if (openFor === marker && !card.hidden) { close(); return; } | |||
close(); | |||
card.querySelector('.gr-note-card-kind').textContent = | |||
(TYPES[note.type] || TYPES.note) + ' [' + num + ']'; | |||
var body = card.querySelector('.gr-note-card-body'); | |||
// A bare text node, deliberately. gr-new-content's handler wraps it, | |||
// records it as translatable and applies whatever script is showing — | |||
// tagging it here instead would make the handler skip it as already | |||
// done, and the note would sit in Devanagari on a Telugu page. | |||
body.textContent = note.text; | |||
card.hidden = false; | |||
place(marker); | |||
try { | |||
window.dispatchEvent(new CustomEvent('gr-new-content', { detail: { container: body } })); | |||
} catch (e) { } | |||
marker.setAttribute('aria-expanded', 'true'); | |||
marker.classList.add('gr-note-marker-open'); | |||
openFor = marker; | |||
} | |||
function decorate() { | |||
var notes = notesFromList(); | |||
var n = 0; | |||
document.querySelectorAll('#mw-content-text ' + HL).forEach(function (hl) { | |||
var id = hl.getAttribute('data-gra-id'); | |||
var note = id && notes[id]; | |||
if (!note || !note.text) return; // orphan highlight: leave it alone | |||
n++; | |||
if (hl.nextElementSibling && | |||
hl.nextElementSibling.classList.contains('gr-note-marker')) { | |||
hl.nextElementSibling.textContent = '[' + n + ']'; | |||
return; | |||
} | |||
var m = document.createElement('sup'); | |||
m.className = 'gr-note-marker'; | |||
m.textContent = '[' + n + ']'; | |||
m.setAttribute('role', 'button'); | |||
m.setAttribute('tabindex', '0'); | |||
m.setAttribute('aria-expanded', 'false'); | |||
var num = n; | |||
function show(e) { e.preventDefault(); e.stopPropagation(); open(m, note, num); } | |||
m.addEventListener('click', show); | |||
m.addEventListener('keydown', function (e) { | |||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(m, note, num); } | |||
}); | |||
// The annotated words open it as well. On a phone the marker is a | |||
// small target, and the words are what the reader is looking at. | |||
hl.addEventListener('click', show); | |||
hl.parentNode.insertBefore(m, hl.nextSibling); | |||
}); | |||
var list = document.querySelector(LIST); | |||
if (list) list.classList.toggle('gr-notes-listed', n > 0); | |||
} | |||
document.addEventListener('click', function (e) { | |||
if (!card || card.hidden) return; | |||
if (!card.contains(e.target)) close(); | |||
}); | |||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); }); | |||
window.addEventListener('resize', function () { if (openFor) place(openFor); }); | |||
window.addEventListener('scroll', function () { if (openFor) place(openFor); }, true); | |||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', decorate); | |||
else decorate(); | |||
if (window.mw && mw.hook) mw.hook('wikipage.content').add(function () { setTimeout(decorate, 150); }); | |||
}()); | }()); | ||