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 &amp; 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;
     // Position to the right of the selection bounding rect
     // FAB uses position:fixed — coords are viewport-relative (no scroll offset needed).
     var scrollY  = window.scrollY || window.pageYOffset;
    // Place strip to the right of the selection; if it would go off-screen, place to the left.
    var scrollX  = window.scrollX || window.pageXOffset;
     var fabW = 46; var fabH = 84;
     var top     = rect.top + scrollY + ( rect.height / 2 ) - 40;
     var top = rect.top + ( rect.height / 2 ) - ( fabH / 2 );
     var left     = rect.right + scrollX + 12;
     var left = rect.right + 10;
     // Keep within viewport
 
     var fabW = 46; var fabH = 90;
     // If too close to right edge, flip to left of selection
     top = clamp( top, scrollY + 8, scrollY + window.innerHeight - fabH - 8 );
     if ( left + fabW > window.innerWidth - 8 ) {
     left = clamp( left, 8,           scrollX + window.innerWidth - fabW - 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 scrollY = window.scrollY || window.pageYOffset;
    // position:fixed — viewport coords only
     var scrollX  = window.scrollX || window.pageXOffset;
     var top = _selRect.bottom + 8;
     var top      = _selRect.bottom + scrollY + 8;
     var left = _selRect.left;
     var left     = _selRect.right  + scrollX + 8;
    // Keep composer within viewport
     var maxLeft  = scrollX + window.innerWidth - 316;
     var composerW = 308;
     left = Math.min( left, maxLeft );
    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]]) 17:22, 23 April 2026 (UTC)';
             + '\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');
       }
       }
     });
     });