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); });
}());
}());