MediaWiki:Gadget-GrAnnotations.js: Difference between revisions
Created page with "/** * gr_annotations.js — grantha.io inline Comments + Bookmarks * ══════════════════════════════════════════════════════════════════════ * * BEHAVIOUR (mirrors Google Docs) * ──────────────────────────────── * 1. User selects text anywhere in mw-content-text...." |
No edit summary |
||
| Line 63: | Line 63: | ||
var ADMIN_USER = 'GranthaGate'; | var ADMIN_USER = 'GranthaGate'; | ||
var CONTENT_SEL = '#mw-content-text'; | var CONTENT_SEL = '#mw-content-text'; | ||
var BM_LS_KEY = 'grantha_bm_' + ( ( window.mw && mw.config.get( 'wgPageName' ) ) || '' ); | var BM_LS_KEY = 'grantha_bm_' + ( ( window.mw && mw.config.get( 'wgPageName' ) ) || '' ); | ||
var CMT_LS_KEY = 'grantha_cmt_' + ( ( window.mw && mw.config.get( 'wgPageName' ) ) || '' ); | |||
var pageTitle = ( window.mw && mw.config.get( 'wgPageName' ) ) || ''; | var pageTitle = ( window.mw && mw.config.get( 'wgPageName' ) ) || ''; | ||
var commentsPage = pageTitle + '/Comments'; | var commentsPage = pageTitle + '/Comments'; | ||
| Line 195: | Line 196: | ||
$backdrop = $('<div id="gra-backdrop"></div>'); | $backdrop = $('<div id="gra-backdrop"></div>'); | ||
$( 'body' ).append( $backdrop ); | $( 'body' ).append( $backdrop ); | ||
// Persistent toggle button — always visible, opens the panel | |||
var $toggle = $( [ | |||
'<button id="gra-toggle" title="Comments & Bookmarks">', | |||
' <span class="gra-icon gra-icon-comment" id="gra-toggle-icon"></span>', | |||
' <span id="gra-toggle-badge"></span>', | |||
'</button>', | |||
].join('') ); | |||
$( 'body' ).append( $toggle ); | |||
$toggle.on( 'click', function() { | |||
if ( $panel.hasClass('gra-panel-open') ) { | |||
closePanel(); | |||
} else { | |||
openPanel( _activeTab ); | |||
} | |||
} ); | |||
// Cache references | // Cache references | ||
$panelBody = $( '#gra-panel-body' ); | $panelBody = $( '#gra-panel-body' ); | ||
// Set panel title to page name | |||
$( '#gra-panel-head div' ).first() | |||
.text( pageTitle.replace(/_/g,' ').split('/')[0].slice(0,30) ) | |||
.css({ 'font-size':'13px', 'font-weight':'600', 'color':'#5a3a00' }); | |||
$tabComments = $( '#gra-tab-comments' ); | $tabComments = $( '#gra-tab-comments' ); | ||
$tabBookmarks = $( '#gra-tab-bookmarks' ); | $tabBookmarks = $( '#gra-tab-bookmarks' ); | ||
| Line 222: | Line 243: | ||
function showFab( rect ) { | function showFab( rect ) { | ||
if ( !rect ) return; | if ( !rect ) return; | ||
// | // FAB uses position:fixed — coords are viewport-relative (no scroll offset needed). | ||
var | // Place strip to the right of the selection; if it would go off-screen, place to the left. | ||
var fabW = 46; var fabH = 84; | |||
var top | var top = rect.top + ( rect.height / 2 ) - ( fabH / 2 ); | ||
var left | var left = rect.right + 10; | ||
// | |||
// If too close to right edge, flip to left of selection | |||
top | if ( left + fabW > window.innerWidth - 8 ) { | ||
left = clamp( left, 8, | left = rect.left - fabW - 10; | ||
} | |||
// Clamp vertically within viewport | |||
top = clamp( top, 8, window.innerHeight - fabH - 8 ); | |||
// Clamp horizontally | |||
left = clamp( left, 8, window.innerWidth - fabW - 8 ); | |||
$fab.css({ top: top + 'px', left: left + 'px' }).addClass('gra-fab-visible'); | $fab.css({ top: top + 'px', left: left + 'px' }).addClass('gra-fab-visible'); | ||
| Line 270: | Line 296: | ||
function positionComposer( $el ) { | function positionComposer( $el ) { | ||
if ( !_selRect ) return; | if ( !_selRect ) return; | ||
var | // position:fixed — viewport coords only | ||
var | var top = _selRect.bottom + 8; | ||
var | var left = _selRect.left; | ||
// Keep composer within viewport | |||
var composerW = 308; | |||
if ( left + composerW > window.innerWidth - 8 ) { | |||
left = window.innerWidth - composerW - 8; | |||
} | |||
left = Math.max( left, 8 ); | |||
// If composer would appear below viewport, show above selection instead | |||
if ( top + 160 > window.innerHeight ) { | |||
top = _selRect.top - 170; | |||
} | |||
top = Math.max( top, 8 ); | |||
$el.css({ top: top + 'px', left: left + 'px' }); | $el.css({ top: top + 'px', left: left + 'px' }); | ||
} | } | ||
| Line 341: | Line 375: | ||
var entry = { id:id, author:currentUser, ts:ts, quote:quote, text:text }; | var entry = { id:id, author:currentUser, ts:ts, quote:quote, text:text }; | ||
_comments.push( entry ); | _comments.push( entry ); | ||
// Persist highlight anchor so it survives page refresh | |||
persistCommentHighlight( id, quote ); | |||
// Persist to wiki | // Persist to wiki | ||
| Line 390: | Line 426: | ||
+ commentText.slice(0,500) | + commentText.slice(0,500) | ||
+ (commentText.length>500?'\n…':'') | + (commentText.length>500?'\n…':'') | ||
+ '\n\n[[User:Chandrashekars|Chandrashekars]] ([[User talk:Chandrashekars|talk]]) | + '\n\n[[User:Chandrashekars|Chandrashekars]] ([[User talk:Chandrashekars|talk]]) 08:59, 24 April 2026 (UTC)'; | ||
new mw.Api().postWithEditToken({ | new mw.Api().postWithEditToken({ | ||
action:'edit', title:adminTalk, section:'new', | action:'edit', title:adminTalk, section:'new', | ||
| Line 730: | Line 766: | ||
// in the content to re-wrap the same span after page reload. | // in the content to re-wrap the same span after page reload. | ||
// (Comments are server-stored and we only re-render cards, not re-wrap.) | // (Comments are server-stored and we only re-render cards, not re-wrap.) | ||
// ── Persist comment highlight anchors to localStorage ──────────────── | |||
// We only store {id, quote} — the full comment data lives on the wiki. | |||
// On reload, restoreCommentHighlights re-wraps the quote text in the page | |||
// so the yellow highlight appears again. | |||
function persistCommentHighlight( id, quote ) { | |||
try { | |||
var stored = JSON.parse( localStorage.getItem( CMT_LS_KEY ) || '[]' ); | |||
// Deduplicate | |||
stored = stored.filter( function(h){ return h.id !== id; } ); | |||
stored.push( { id: id, quote: quote } ); | |||
localStorage.setItem( CMT_LS_KEY, JSON.stringify( stored ) ); | |||
} catch(e){} | |||
} | |||
function restoreCommentHighlights() { | |||
var stored = []; | |||
try { stored = JSON.parse( localStorage.getItem( CMT_LS_KEY ) || '[]' ); } catch(e){} | |||
stored.forEach( function(h) { | |||
if ( !h.quote || !h.id ) return; | |||
if ( document.querySelector( '[data-gra-id="' + h.id + '"].gra-comment-highlight' ) ) return; | |||
var needle = h.quote.replace(/…$/, '').trim().slice(0, 80); | |||
if ( !needle ) return; | |||
var range = findTextInContent( document.querySelector(CONTENT_SEL), needle ); | |||
if ( range ) { | |||
var span = document.createElement('span'); | |||
span.className = 'gra-comment-highlight'; | |||
span.setAttribute('data-gra-id', h.id); | |||
try { range.surroundContents(span); } catch(e){} | |||
} | |||
}); | |||
} | |||
function restoreBookmarkHighlights() { | function restoreBookmarkHighlights() { | ||
| Line 794: | Line 863: | ||
loadBookmarks(); | loadBookmarks(); | ||
restoreBookmarkHighlights(); | restoreBookmarkHighlights(); | ||
restoreCommentHighlights(); | |||
// Pre-load comment count in background | // Pre-load comment count in background | ||
loadComments(function(){ | loadComments(function(){ | ||
| Line 802: | Line 872: | ||
'font-size:10px;padding:0 5px;margin-left:2px;">' + _comments.length + '</span>' | 'font-size:10px;padding:0 5px;margin-left:2px;">' + _comments.length + '</span>' | ||
); | ); | ||
// Update toggle badge | |||
var $badge = $( '#gra-toggle-badge' ); | |||
if ( $badge.length ) $badge.text( _comments.length ).css('display','flex'); | |||
} | } | ||
}); | }); | ||