MediaWiki:Gadget-GrAnnotations.js: Difference between revisions

No edit summary
No edit summary
Line 800: Line 800:
     closeNoteComposer();
     closeNoteComposer();
     openPanel('notes');
     openPanel('notes');
  }
  /* ── SHAREABLE CITATION ────────────────────────────────────────────
    A bookmark is personal, but what it produces is not: a reference in
    APA form plus a link that resolves for anyone.
    The link cannot point at the bookmark itself. A bookmark wraps the
    reader's own selection in a span with an id generated in their
    browser; that span is never saved to the page, so #<bookmark-id>
    resolves for nobody else. The citation therefore anchors to the
    nearest enclosing block that the PUBLISHED page carries an id for -
    the same ids copyId.js links to, and the same ones cross-references
    across the site already use.
    APA 7th, for a work on a website with no date:
      Author. (n.d.). Title (Locator). Site. Retrieved D Month Y, from URL
    n.d. because these works carry no publication date, and a retrieval
    date because a wiki page can change. The block reference is given as
    a parenthetical locator after the title, which is where APA puts a
    section or part designation. */
  var CITE_BLOCK_SEL = '.verse-block[id], .bhashyam-block[id], .teeka-block[id], ' +
                      '[data-block-id][id], .gr-apparatus[id], .shloka[id]';
  function citeAnchorFor(node) {
    var el = node && node.nodeType === 3 ? node.parentNode : node;
    while (el && el !== document.body) {
      if (el.matches && el.matches(CITE_BLOCK_SEL)) return el;
      el = el.parentNode;
    }
    return null;
  }
  // Walks up from the cited block: an interleaved commentary block states
  // its own work, otherwise the page's title element does.
  function citeSourceFor(el) {
    var n = el;
    while (n && n !== document.body) {
      if (n.getAttribute && n.getAttribute('data-cite-title')) {
        return {
          author: n.getAttribute('data-cite-author') || '',
          title:  n.getAttribute('data-cite-title') || '',
          page:  n.getAttribute('data-cite-page') || ''
        };
      }
      n = n.parentNode;
    }
    var t = document.querySelector('.gr-doc-title');
    return {
      author: (t && t.getAttribute('data-author')) || '',
      title:  (t && (t.getAttribute('data-doc-title') || t.textContent.trim())) || document.title,
      page:  (window.mw && mw.config.get('wgPageName')) || ''
    };
  }
  function citeUrl(pageName, anchorId) {
    var path = (window.mw && mw.config.get('wgArticlePath')) || '/wiki/$1';
    var page = pageName || (window.mw && mw.config.get('wgPageName')) || '';
    var base = path.replace('$1', String(page).split('/').map(encodeURIComponent).join('/'));
    return location.origin + base + (anchorId ? '#' + encodeURIComponent(anchorId) : '');
  }
  var CITE_MONTHS = ['January','February','March','April','May','June',
                    'July','August','September','October','November','December'];
  function retrievedToday() {
    var d = new Date();
    return CITE_MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear();
  }
  function siteName() {
    return (window.mw && mw.config.get('wgSiteName')) || location.hostname;
  }
  // rec: the bookmark record. Returns {text, url} - text is the full APA
  // reference with the link in it, ready to paste anywhere.
  function buildCitation(rec) {
    var src = rec.citeSource || {};
    var author = src.author || '';
    var title  = src.title || '';
    var anchor = rec.anchor || '';
    var url    = citeUrl(src.page, anchor);
    var parts  = [];
    if (author) parts.push(author + '.');
    parts.push('(n.d.).');
    parts.push(title + (anchor ? ' (' + anchor + ')' : '') + '.');
    parts.push(siteName() + '.');
    parts.push('Retrieved ' + retrievedToday() + ', from ' + url);
    return { text: parts.join(' '), url: url };
  }
  function copyToClipboard(text, done) {
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text).then(done).catch(function () { legacyCopy(text, done); });
    } else { legacyCopy(text, done); }
  }
  function legacyCopy(text, done) {
    var ta = document.createElement('textarea');
    ta.value = text;
    ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;';
    document.body.appendChild(ta); ta.focus(); ta.select();
    try { document.execCommand('copy'); done && done(); } catch (e) {}
    document.body.removeChild(ta);
   }
   }


Line 828: Line 932:
     var quote = _selText.slice(0,120) + (_selText.length > 120 ? '…' : '');
     var quote = _selText.slice(0,120) + (_selText.length > 120 ? '…' : '');
     if (!_selRange && _selText) reCaptureFromDOM();
     if (!_selRange && _selText) reCaptureFromDOM();
    // Resolve the citation anchor BEFORE wrapping: wrapSelection inserts a
    // span of its own, and resolving afterwards would just find that.
    var anchorEl = citeAnchorFor(_selRange && _selRange.startContainer);
     var span  = wrapSelection(id, 'gra-bookmark-highlight');
     var span  = wrapSelection(id, 'gra-bookmark-highlight');
     if (span) { span.setAttribute('data-gra-id', id); span.setAttribute('data-gra-name', name); }
     if (span) { span.setAttribute('data-gra-id', id); span.setAttribute('data-gra-name', name); }
     var rec = {id:id, name:name, quote:quote, ts:nowIso()};
    if (!anchorEl && span) anchorEl = citeAnchorFor(span);
     var rec = {id:id, name:name, quote:quote, ts:nowIso(),
              anchor: anchorEl ? (anchorEl.id || anchorEl.getAttribute('data-block-id') || '') : '',
              citeSource: citeSourceFor(anchorEl || document.body)};
     _bookmarks.push(rec);
     _bookmarks.push(rec);
     Store.cache();
     Store.cache();
Line 914: Line 1,024:
     var html = '';
     var html = '';
     _bookmarks.slice().reverse().forEach(function(b){
     _bookmarks.slice().reverse().forEach(function(b){
      var cite = buildCitation(b);
       html += '<div class="gra-bookmark-card" data-gra-id="'+esc(b.id)+'">'
       html += '<div class="gra-bookmark-card" data-gra-id="'+esc(b.id)+'">'
             + '<span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>'
             + '<span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>'
Line 919: Line 1,030:
             + '<div class="gra-bookmark-name">'+esc(b.name)+'</div>'
             + '<div class="gra-bookmark-name">'+esc(b.name)+'</div>'
             + (b.quote ? '<div class="gra-bookmark-quote">'+esc(b.quote)+'</div>' : '')
             + (b.quote ? '<div class="gra-bookmark-quote">'+esc(b.quote)+'</div>' : '')
            + '<div class="gra-bookmark-cite">'+esc(cite.text)+'</div>'
            + '<button type="button" class="gra-cite-copy" data-cite-id="'+esc(b.id)+'">'
            + 'Copy citation</button>'
             + '</div>'
             + '</div>'
             + '<button class="gra-bookmark-del" data-del-id="'+esc(b.id)+'" title="Remove">×</button>'
             + '<button class="gra-bookmark-del" data-del-id="'+esc(b.id)+'" title="Remove">×</button>'
Line 1,213: Line 1,327:
     $paneBookmarks.on('click', '.gra-bookmark-card', function(e){
     $paneBookmarks.on('click', '.gra-bookmark-card', function(e){
       if ($(e.target).hasClass('gra-bookmark-del')) return;
       if ($(e.target).hasClass('gra-bookmark-del')) return;
      if ($(e.target).hasClass('gra-cite-copy')) return;
       var id = $(this).attr('data-gra-id');
       var id = $(this).attr('data-gra-id');
       if (id) { closePanel(); scrollToHighlight(id); }
       if (id) { closePanel(); scrollToHighlight(id); }
    });
    $paneBookmarks.on('click', '.gra-cite-copy', function(e){
      e.stopPropagation();
      var $btn = $(this), id = $btn.attr('data-cite-id');
      var rec = _bookmarks.filter(function(b){ return b.id === id; })[0];
      if (!rec) return;
      copyToClipboard(buildCitation(rec).text, function(){
        var was = $btn.text();
        $btn.text('Copied').addClass('gra-cite-copy--done');
        setTimeout(function(){ $btn.text(was).removeClass('gra-cite-copy--done'); }, 1600);
      });
     });
     });
     $paneBookmarks.on('click', '.gra-bookmark-del', function(e){
     $paneBookmarks.on('click', '.gra-bookmark-del', function(e){