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){ | ||