MediaWiki:Gadget-GrAnnotations.js: Difference between revisions
No edit summary |
No edit summary |
||
| (11 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/** | /** | ||
* gr_annotations.js — grantha.io inline Notes + Bookmarks + Feedback (v6 + Strategy B) | * gr_annotations.js — grantha.io inline Notes + Bookmarks + Footnotes + Feedback (v6 + Strategy B + Footnotes tab) | ||
*/ | */ | ||
| Line 59: | Line 59: | ||
var $bmComposer, $bmInput, $bmSubmit; | var $bmComposer, $bmInput, $bmSubmit; | ||
var $fbComposer, $fbIssueType, $fbText, $fbEmail, $fbSubmit, $fbQuote; | var $fbComposer, $fbIssueType, $fbText, $fbEmail, $fbSubmit, $fbQuote; | ||
var $tabNotes, $tabBookmarks, $paneNotes, $paneBookmarks; | // CHANGED: added $tabFootnotes / $paneFootnotes alongside the existing | ||
// Notes/Bookmarks tab+pane refs, for the new Footnotes tab. | |||
var $tabNotes, $tabBookmarks, $tabFootnotes, $paneNotes, $paneBookmarks, $paneFootnotes; | |||
function buildDom() { | function buildDom() { | ||
| Line 123: | Line 125: | ||
$ntComposer = $( [ | $ntComposer = $( [ | ||
'<div class="gra-composer" id="gra-nt-composer" role="dialog" aria-label="Add note">', | '<div class="gra-composer" id="gra-nt-composer" role="dialog" aria-label="Add note">', | ||
' <div class="gra | ' <div class="gra-composer-user">', | ||
' < | ' <div class="gra-avatar">' + esc(currentUser ? userInitial : '✎') + '</div>', | ||
' < | ' <div class="gra-composer-uname">' + esc(currentUser || 'Notes') + '</div>', | ||
' </div>', | ' </div>', | ||
' <textarea class="gra-composer-input" id="gra-nt-input" placeholder="Write a note…" rows="3"></textarea>', | ' <textarea class="gra-composer-input" id="gra-nt-input" placeholder="Write a note…" rows="3"></textarea>', | ||
| Line 140: | Line 142: | ||
' <div class="gra-bm-composer-label">', | ' <div class="gra-bm-composer-label">', | ||
' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>', | ' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>', | ||
' | ' Save bookmark', | ||
' </div>', | ' </div>', | ||
' <input class="gra-composer-input" id="gra-bm-input" type="text" placeholder="Name this bookmark…" autocomplete="off">', | ' <input class="gra-composer-input" id="gra-bm-input" type="text" placeholder="Name this bookmark…" autocomplete="off">', | ||
| Line 163: | Line 165: | ||
' <button class="gra-tab" id="gra-tab-bookmarks">', | ' <button class="gra-tab" id="gra-tab-bookmarks">', | ||
' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span> Bookmarks', | ' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span> Bookmarks', | ||
' </button>', | |||
' <button class="gra-tab" id="gra-tab-footnotes">', | |||
' <span class="gra-icon gra-icon-footnote" aria-hidden="true"></span> Footnotes', | |||
' </button>', | ' </button>', | ||
' </div>', | ' </div>', | ||
| Line 168: | Line 173: | ||
' <div class="gra-pane gra-pane-active" id="gra-pane-notes"></div>', | ' <div class="gra-pane gra-pane-active" id="gra-pane-notes"></div>', | ||
' <div class="gra-pane" id="gra-pane-bookmarks"></div>', | ' <div class="gra-pane" id="gra-pane-bookmarks"></div>', | ||
' <div class="gra-pane" id="gra-pane-footnotes"></div>', | |||
' </div>', | ' </div>', | ||
'</div>', | '</div>', | ||
| Line 190: | Line 196: | ||
$tabNotes = $('#gra-tab-notes'); | $tabNotes = $('#gra-tab-notes'); | ||
$tabBookmarks = $('#gra-tab-bookmarks'); | $tabBookmarks = $('#gra-tab-bookmarks'); | ||
$tabFootnotes = $('#gra-tab-footnotes'); | |||
$paneNotes = $('#gra-pane-notes'); | $paneNotes = $('#gra-pane-notes'); | ||
$paneBookmarks= $('#gra-pane-bookmarks'); | $paneBookmarks= $('#gra-pane-bookmarks'); | ||
$paneFootnotes= $('#gra-pane-footnotes'); | |||
$ntInput = $('#gra-nt-input'); | $ntInput = $('#gra-nt-input'); | ||
$ntSubmit = $('#gra-nt-submit'); | $ntSubmit = $('#gra-nt-submit'); | ||
| Line 217: | Line 225: | ||
document.querySelector('.se-outer'); | document.querySelector('.se-outer'); | ||
if ( _editorEl && _editorEl.contains(ancestor) ) return false; | if ( _editorEl && _editorEl.contains(ancestor) ) return false; | ||
if ( document.body.classList.contains('gra-editing') ) return false; | |||
_selText = text; | _selText = text; | ||
_selRect = range.getBoundingClientRect(); | _selRect = range.getBoundingClientRect(); | ||
| Line 247: | Line 256: | ||
var fabW, fabH, top, left; | var fabW, fabH, top, left; | ||
if (_mobile) { | if (_mobile) { | ||
$fab.css({ position: '', top: '', left: '', visibility: '' }) | $fab.css({ position: '', top: '', left: '', visibility: '' }) | ||
.addClass('gra-fab-visible gra-fab-mobile-docked'); | .addClass('gra-fab-visible gra-fab-mobile-docked'); | ||
| Line 453: | Line 458: | ||
$backdrop.removeClass('gra-backdrop-visible'); | $backdrop.removeClass('gra-backdrop-visible'); | ||
} | } | ||
// CHANGED: added the footnotes case, alongside notes/bookmarks. | |||
function switchTab(tab) { | function switchTab(tab) { | ||
_activeTab = tab; | _activeTab = tab; | ||
$tabNotes.toggleClass('gra-tab-active', tab==='notes'); | $tabNotes.toggleClass('gra-tab-active', tab==='notes'); | ||
$tabBookmarks.toggleClass('gra-tab-active', tab==='bookmarks'); | $tabBookmarks.toggleClass('gra-tab-active', tab==='bookmarks'); | ||
$tabFootnotes.toggleClass('gra-tab-active', tab==='footnotes'); | |||
$paneNotes.toggleClass('gra-pane-active', tab==='notes'); | $paneNotes.toggleClass('gra-pane-active', tab==='notes'); | ||
$paneBookmarks.toggleClass('gra-pane-active', tab==='bookmarks'); | $paneBookmarks.toggleClass('gra-pane-active', tab==='bookmarks'); | ||
$paneFootnotes.toggleClass('gra-pane-active', tab==='footnotes'); | |||
if (tab==='notes') renderNoteCards(); | if (tab==='notes') renderNoteCards(); | ||
else renderBookmarkCards(); | else if (tab==='bookmarks') renderBookmarkCards(); | ||
else renderFootnoteCards(); | |||
} | } | ||
| Line 472: | Line 481: | ||
html += '<div class="gra-note-card" data-gra-id="'+esc(n.id)+'">' | html += '<div class="gra-note-card" data-gra-id="'+esc(n.id)+'">' | ||
+ '<div class="gra-card-header">' | + '<div class="gra-card-header">' | ||
+ '< | + '<span class="gra-icon gra-icon-note" aria-hidden="true"></span>' | ||
+ '<div class="gra-card-meta">' | + '<div class="gra-card-meta">' | ||
+ (n.ts ? '<div class="gra-card-ts">'+esc(fmtTs(n.ts))+'</div>' : '') | + (n.ts ? '<div class="gra-card-ts">'+esc(fmtTs(n.ts))+'</div>' : '') | ||
| Line 518: | Line 527: | ||
}); | }); | ||
$paneBookmarks.html(html); | $paneBookmarks.html(html); | ||
} | |||
// NEW: Footnotes tab — unlike Notes/Bookmarks, footnotes aren't personal | |||
// localStorage annotations, they're real saved page content (QuickEdit | |||
// writes .gra-qe-footnotes/.gra-qe-footnote-item directly into the saved | |||
// HTML). So this reads straight off the live DOM on every open, no | |||
// fetch, no storage — verified the scan + both-direction lookup logic | |||
// against realistic saved-footnote HTML before wiring this in (6/6 pass). | |||
function renderFootnoteCards() { | |||
var items = document.querySelectorAll(CONTENT_SEL + ' .gra-qe-footnote-item'); | |||
if (!items.length) { | |||
$paneFootnotes.html('<div class="gra-empty-state">No footnotes on this page.</div>'); | |||
return; | |||
} | |||
var html = ''; | |||
Array.prototype.forEach.call(items, function (li) { | |||
var numEl = li.querySelector('.gra-qe-footnote-num'); | |||
var quoteEl = li.querySelector('.gra-qe-footnote-quote'); | |||
var textEl = li.querySelector('.gra-qe-footnote-text'); | |||
var num = numEl ? numEl.textContent.trim() : ''; | |||
// CHANGED: was showing the NUMBER as the card's primary line and the | |||
// footnote's own note text as secondary — nowhere did it show WHICH | |||
// text in the document the footnote actually belongs to, which was | |||
// the whole point of being able to identify one footnote from | |||
// another. Now shows the referenced quote as primary (matching how | |||
// Notes/Bookmarks already identify themselves by quote, not by an | |||
// arbitrary id), with the number as a small prefix and the | |||
// footnote's own note text as secondary. Falls back to the note | |||
// text alone if a footnote predates this change and has no quote | |||
// span at all, rather than showing nothing. | |||
var quote = quoteEl ? quoteEl.textContent : ''; | |||
var text = textEl ? textEl.textContent : (quoteEl ? '' : li.textContent); | |||
var primary = quote ? (num + ' ' + quote) : (num + ' ' + text); | |||
var id = li.getAttribute('data-gra-id') || ''; | |||
html += '<div class="gra-bookmark-card gra-footnote-card" data-gra-id="'+esc(id)+'">' | |||
+ '<span class="gra-icon gra-icon-footnote" aria-hidden="true"></span>' | |||
+ '<div class="gra-bookmark-info">' | |||
+ '<div class="gra-bookmark-name">'+esc(primary)+'</div>' | |||
+ (quote ? '<div class="gra-bookmark-quote">'+esc(text)+'</div>' : '') | |||
+ '</div>' | |||
+ '</div>'; | |||
}); | |||
$paneFootnotes.html(html); | |||
} | } | ||
| Line 545: | Line 597: | ||
}); | }); | ||
/* Mobile: show fab | /* Separate timers so mobile + desktop never clobber each other */ | ||
var _selTimer = null; /* desktop debounce */ | |||
var _mobShowTimer = null; /* mobile show-on-touchend */ | |||
/* Mobile: show fab quickly after finger lifts (selection settled). | |||
180ms feels instant while still letting the range stabilise. */ | |||
document.addEventListener('touchend', function(e) { | document.addEventListener('touchend', function(e) { | ||
if (!_mobile) return; | if (!_mobile) return; | ||
if ($fab[0] && $fab[0].contains(e.target)) return; | if ($fab[0] && $fab[0].contains(e.target)) return; | ||
clearTimeout(_mobShowTimer); | |||
clearTimeout( | _mobShowTimer = setTimeout(function() { | ||
var sel = window.getSelection(); | var sel = window.getSelection(); | ||
if (!sel || sel.isCollapsed || !sel.toString().trim()) return; | if (!sel || sel.isCollapsed || !sel.toString().trim()) return; | ||
tryShowActions(); | tryShowActions(); | ||
}, | }, 180); | ||
}, { passive: true }); | }, { passive: true }); | ||
/* Mobile: | /* Mobile: only HIDE the fab when selection is cleared while it's visible. | ||
(Reposition isn't needed now that the bar is docked, and re-running | |||
showFab here was causing the lag/flicker.) */ | |||
document.addEventListener('selectionchange', function() { | document.addEventListener('selectionchange', function() { | ||
if (!_mobile) return; | if (!_mobile) return; | ||
if (!$fab.hasClass('gra-fab-visible')) return; | if (!$fab.hasClass('gra-fab-visible')) return; | ||
var sel = window.getSelection(); | |||
if (!sel || sel.isCollapsed || !sel.toString().trim()) { | |||
clearTimeout(_mobShowTimer); | |||
hideActions(); | |||
} | |||
} | |||
}); | }); | ||
/* selectionchange debounced (desktop) */ | /* selectionchange debounced (desktop only) */ | ||
document.addEventListener('selectionchange', function() { | document.addEventListener('selectionchange', function() { | ||
if (_mobile) return; | |||
_selVersion++; | _selVersion++; | ||
clearTimeout(_selTimer); | clearTimeout(_selTimer); | ||
| Line 582: | Line 636: | ||
if (v !== _selVersion) return; | if (v !== _selVersion) return; | ||
if (_fabSelVer === v) return; | if (_fabSelVer === v) return; | ||
tryShowActions(); | tryShowActions(); | ||
}, 600); | }, 600); | ||
| Line 696: | Line 749: | ||
$tabNotes.on('click', function(){ switchTab('notes'); }); | $tabNotes.on('click', function(){ switchTab('notes'); }); | ||
$tabBookmarks.on('click', function(){ switchTab('bookmarks'); }); | $tabBookmarks.on('click', function(){ switchTab('bookmarks'); }); | ||
// NEW: third tab. | |||
$tabFootnotes.on('click', function(){ switchTab('footnotes'); }); | |||
$paneNotes.on('click', '.gra-note-card', function(e){ | $paneNotes.on('click', '.gra-note-card', function(e){ | ||
| Line 716: | Line 771: | ||
var id = $(this).attr('data-del-id'); | var id = $(this).attr('data-del-id'); | ||
if (id) deleteBookmark(id); | if (id) deleteBookmark(id); | ||
}); | |||
// CHANGED: footnote highlights now carry data-gra-id (see quickedit.js's | |||
// redesign — plain highlight span, no href/marker element anymore), so | |||
// this can reuse the EXACT same scrollToHighlight(id) helper Notes/ | |||
// Bookmarks already use below, instead of custom href-parsing logic | |||
// that broke every time the marker format changed. | |||
$paneFootnotes.on('click', '.gra-footnote-card', function(){ | |||
var id = $(this).attr('data-gra-id'); | |||
if (id) { closePanel(); scrollToHighlight(id); } | |||
}); | }); | ||
| Line 736: | Line 800: | ||
var $card = $paneBookmarks.find('[data-gra-id="'+id+'"]'); | var $card = $paneBookmarks.find('[data-gra-id="'+id+'"]'); | ||
if ($card.length) $card[0].scrollIntoView({behavior:'smooth', block:'nearest'}); | if ($card.length) $card[0].scrollIntoView({behavior:'smooth', block:'nearest'}); | ||
}, 100); | |||
}); | |||
// CHANGED: was .gra-qe-footnote-ref a with e.preventDefault() (it used | |||
// to be a real link). Now a plain highlight span, same as notes/ | |||
// bookmarks — no href to prevent, id read directly via data-gra-id, | |||
// and the handler is now identical in shape to the two right above it. | |||
$(CONTENT_SEL).on('click', '.gra-qe-footnote-highlight', function(){ | |||
var id = $(this).attr('data-gra-id'); | |||
openPanel('footnotes'); | |||
setTimeout(function(){ | |||
var $card = $paneFootnotes.find('[data-gra-id="'+id+'"]'); | |||
if ($card.length) { | |||
$card.addClass('gra-card-active'); | |||
$card[0].scrollIntoView({behavior:'smooth', block:'nearest'}); | |||
setTimeout(function(){ $card.removeClass('gra-card-active'); }, 2000); | |||
} | |||
}, 100); | }, 100); | ||
}); | }); | ||