MediaWiki:Gadget-GrAnnotations.js: Difference between revisions
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
/** | /** | ||
* gr_annotations.js — grantha.io inline Comments + Bookmarks | * gr_annotations.js — grantha.io inline Comments + Bookmarks (v2) | ||
* ══════════════════════════════════════════════════════════════════════ | * ══════════════════════════════════════════════════════════════════════ | ||
* | * | ||
* | * CHANGES FROM v1 | ||
* | * ──────────────── | ||
* 1. User selects text anywhere in mw-content-text. | * • Mobile support: selectionchange + touchend events trigger the FAB | ||
* 2. | * on iOS/Android. Long-press to select text now shows the comment/ | ||
* bookmark strip correctly. | |||
* • Anonymous comments: any visitor (logged-in or not) can comment. | |||
* A name field is shown to anonymous users. Comments are stored on | |||
* Talk:<PageTitle>/GrComments as before and emailed to | |||
* feedback@anandamakaranda.in via mailto: (anon) or MW EmailUser | |||
* (logged-in, falls back to mailto: if email not configured). | |||
* • Email target changed to feedback@anandamakaranda.in. | |||
* • "Add comment" tooltip localised to "टिप्पणी". | |||
* • "Bookmark" tooltip localised to "चिह्नांकन". | |||
* | |||
* BEHAVIOUR | |||
* ────────── | |||
* 1. User selects text anywhere in #mw-content-text (desktop mouse or | |||
* mobile long-press). | |||
* 2. FAB strip (Comment / Bookmark) appears near the selection. | |||
* 3. Clicking Comment: | * 3. Clicking Comment: | ||
* | * – Opens composer. Anonymous users see a "Your name" field. | ||
* | * – On submit: wraps selection in yellow highlight, saves to wiki, | ||
* emails feedback@anandamakaranda.in, updates panel. | |||
* | |||
* 4. Clicking Bookmark: | * 4. Clicking Bookmark: | ||
* | * – Opens name composer. Saves to localStorage, wraps in blue highlight. | ||
* 5. Right panel (slide-in overlay): | |||
* – Tab 1: Comments (loaded from Talk page) | |||
* – Tab 2: Bookmarks (localStorage) | |||
* 5. Right panel | |||
* | |||
* | |||
* | * | ||
* STORAGE | * STORAGE | ||
* ─────── | * ─────── | ||
* Comments → | * Comments → Talk:<PageTitle>/GrComments ({{GrComment|…}} templates) | ||
* Bookmarks → localStorage key grantha_bm_<pageName> | |||
* Highlight anchors → localStorage key grantha_cmt_<pageName> | |||
* Bookmarks → localStorage | |||
* | |||
* | * | ||
* | * NOTIFICATION | ||
* ──────────── | * ──────────── | ||
* | * Logged-in : MW EmailUser API → feedback@anandamakaranda.in (admin account) | ||
* Falls back to Talk page notification + mailto: link. | |||
* | * Anonymous : Opens mailto:feedback@anandamakaranda.in in a new tab. | ||
* | * The comment is still stored on the wiki Talk page. | ||
* | |||
* ══════════════════════════════════════════════════════════════════════ | * ══════════════════════════════════════════════════════════════════════ | ||
*/ | */ | ||
| Line 64: | Line 52: | ||
// ── Configuration ──────────────────────────────────────────────────── | // ── Configuration ──────────────────────────────────────────────────── | ||
var | var FEEDBACK_EMAIL = 'feedback@anandamakaranda.in'; | ||
var CONTENT_SEL = '#mw-content-text'; | var CONTENT_SEL = '#mw-content-text'; | ||
var BM_LS_KEY | var BM_LS_KEY = 'grantha_bm_' + ( ( window.mw && mw.config.get( 'wgPageName' ) ) || '' ); | ||
var CMT_LS_KEY | var CMT_LS_KEY = 'grantha_cmt_' + ( ( window.mw && mw.config.get( 'wgPageName' ) ) || '' ); | ||
var pageTitle | var pageTitle = ( window.mw && mw.config.get( 'wgPageName' ) ) || ''; | ||
var commentsPage = 'Talk:' + pageTitle + '/GrComments'; | |||
var currentUser = ( window.mw && mw.config.get( 'wgUserName' ) ) || ''; | |||
var userInitial = currentUser ? currentUser.charAt( 0 ).toUpperCase() : '?'; | |||
var commentsPage = 'Talk:' + pageTitle + '/GrComments'; | |||
var currentUser | |||
var userInitial | |||
// | // Run on all content namespaces except the /GrComments page itself | ||
if ( window.mw ) { | if ( window.mw ) { | ||
var ns = mw.config.get( 'wgNamespaceNumber' ); | var ns = mw.config.get( 'wgNamespaceNumber' ); | ||
if ( ns < 0 ) return; | if ( ns < 0 ) return; | ||
if ( /\/ | if ( /\/GrComments$/.test( pageTitle ) ) return; | ||
} | } | ||
// ── State ──────────────────────────────────────────────────────────── | // ── State ──────────────────────────────────────────────────────────── | ||
var _selRange = null; | |||
var _selRange = null; | var _selText = ''; | ||
var _selText = ''; | var _selRect = null; | ||
var _selRect = null; | var _comments = []; | ||
var _comments = []; | var _bookmarks = []; | ||
var _bookmarks = []; | |||
var _cmtLoaded = false; | var _cmtLoaded = false; | ||
var _activeTab = 'comments'; // | var _activeTab = 'comments'; | ||
// Mobile: track whether FAB was dismissed for the current selection | |||
var _selVersion = 0; // incremented on each new selectionchange | |||
var _fabSelVer = -1; // the _selVersion when FAB was last shown | |||
// ── Helpers ────────────────────────────────────────────────────────── | // ── Helpers ────────────────────────────────────────────────────────── | ||
| Line 112: | Line 100: | ||
} catch(e){ return ts; } | } catch(e){ return ts; } | ||
} | } | ||
function clamp( | function clamp( v, lo, hi ) { return Math.max(lo, Math.min(hi, v)); } | ||
function isMobile() { return window.innerWidth < 768 || 'ontouchstart' in window; } | |||
// ── DOM references | // ── DOM references ─────────────────────────────────────────────────── | ||
var $fab, $panel, $backdrop | var $fab, $panel, $backdrop; | ||
var $cmpComposer, $cmpInput, $cmpSubmit; | var $cmpComposer, $cmpInput, $cmpSubmit, $cmpName; | ||
var $bmComposer, $bmInput, $bmSubmit; | var $bmComposer, $bmInput, $bmSubmit; | ||
var $tabComments, $tabBookmarks; | var $tabComments, $tabBookmarks; | ||
| Line 127: | Line 116: | ||
function buildDom() { | function buildDom() { | ||
// ── | // ── FAB strip ──────────────────────────────────────────────────── | ||
$fab = $( [ | $fab = $( [ | ||
'<div id="gra-fab">', | '<div id="gra-fab" role="toolbar" aria-label="टिप्पणी / चिह्नांकन">', | ||
' <button class="gra-fab-btn" id="gra-fab-comment" type="button">', | ' <button class="gra-fab-btn" id="gra-fab-comment" type="button" aria-label="टिप्पणी जोड़ें">', | ||
' <span class="gra-icon gra-icon-comment"></span>', | ' <span class="gra-icon gra-icon-comment" aria-hidden="true"></span>', | ||
' <span class="gra-fab-tooltip"> | ' <span class="gra-fab-tooltip">टिप्पणी</span>', | ||
' </button>', | ' </button>', | ||
' <button class="gra-fab-btn" id="gra-fab-bookmark" type="button">', | ' <button class="gra-fab-btn" id="gra-fab-bookmark" type="button" aria-label="चिह्नांकन">', | ||
' <span class="gra-icon gra-icon-bookmark"></span>', | ' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>', | ||
' <span class="gra-fab-tooltip"> | ' <span class="gra-fab-tooltip">चिह्नांकन</span>', | ||
' </button>', | ' </button>', | ||
'</div>', | '</div>', | ||
| Line 142: | Line 131: | ||
$( 'body' ).append( $fab ); | $( 'body' ).append( $fab ); | ||
// ── Comment composer | // ── Comment composer ───────────────────────────────────────────── | ||
// Anonymous users see an extra "Your name" field (hidden for logged-in) | |||
var nameRow = currentUser ? '' : [ | |||
' <div class="gra-composer-name-row">', | |||
' <input class="gra-composer-input gra-name-input" id="gra-cmp-name"', | |||
' type="text" placeholder="आपका नाम (ऐच्छिक)" autocomplete="name">', | |||
' </div>', | |||
].join(''); | |||
$cmpComposer = $( [ | $cmpComposer = $( [ | ||
'<div class="gra-composer" id="gra-cmp-composer">', | '<div class="gra-composer" id="gra-cmp-composer" role="dialog" aria-label="टिप्पणी जोड़ें">', | ||
' <div class="gra-composer-user">', | ' <div class="gra-composer-user">', | ||
' <div class="gra-avatar" id="gra-cmp-avatar">' + esc(userInitial) + '</div>', | ' <div class="gra-avatar" id="gra-cmp-avatar">' + esc(currentUser ? userInitial : '?') + '</div>', | ||
' <div class="gra-composer- | ' <div class="gra-composer-uname">' + esc(currentUser || 'अतिथि') + '</div>', | ||
' </div>', | ' </div>', | ||
nameRow, | |||
' <textarea class="gra-composer-input" id="gra-cmp-input"', | ' <textarea class="gra-composer-input" id="gra-cmp-input"', | ||
' placeholder=" | ' placeholder="टिप्पणी लिखें…" rows="3"></textarea>', | ||
' <div class="gra-composer-actions">', | ' <div class="gra-composer-actions">', | ||
' <button class="gra-btn-cancel" id="gra-cmp-cancel"> | ' <button class="gra-btn-cancel" id="gra-cmp-cancel">रद्द</button>', | ||
' <button class="gra-btn-submit" id="gra-cmp-submit" disabled> | ' <button class="gra-btn-submit" id="gra-cmp-submit" disabled>भेजें</button>', | ||
' </div>', | ' </div>', | ||
'</div>', | '</div>', | ||
].join('') ); | ].join('') ); | ||
$( 'body' ).append( $cmpComposer ); | $( 'body' ).append( $cmpComposer ); | ||
// ── Bookmark composer | // ── Bookmark composer ───────────────────────────────────────────── | ||
$bmComposer = $( [ | $bmComposer = $( [ | ||
'<div class="gra-bm-composer" id="gra-bm-composer">', | '<div class="gra-bm-composer" id="gra-bm-composer" role="dialog" aria-label="चिह्नांकन">', | ||
' <div class="gra-bm-composer-label">', | ' <div class="gra-bm-composer-label">', | ||
' <span class="gra-icon gra-icon-bookmark"></span>', | ' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>', | ||
' | ' चिह्नांकन सहेजें', | ||
' </div>', | ' </div>', | ||
' <input class="gra-composer-input" id="gra-bm-input"', | ' <input class="gra-composer-input" id="gra-bm-input"', | ||
' type="text" placeholder=" | ' type="text" placeholder="नाम दें…" autocomplete="off">', | ||
' <div class="gra-composer-actions">', | ' <div class="gra-composer-actions">', | ||
' <button class="gra-btn-cancel" id="gra-bm-cancel"> | ' <button class="gra-btn-cancel" id="gra-bm-cancel">रद्द</button>', | ||
' <button class="gra-btn-submit" id="gra-bm-submit"> | ' <button class="gra-btn-submit" id="gra-bm-submit">सहेजें</button>', | ||
' </div>', | ' </div>', | ||
'</div>', | '</div>', | ||
| Line 178: | Line 176: | ||
// ── Right panel ─────────────────────────────────────────────────── | // ── Right panel ─────────────────────────────────────────────────── | ||
$panel = $( [ | $panel = $( [ | ||
'<div id="gra-panel">', | '<div id="gra-panel" role="complementary" aria-label="टिप्पणियाँ">', | ||
' <div id="gra-panel-head">', | ' <div id="gra-panel-head">', | ||
' <div></div>', | ' <div id="gra-panel-title"></div>', | ||
' <button id="gra-panel-close" title=" | ' <button id="gra-panel-close" title="बन्द करें">✕</button>', | ||
' </div>', | ' </div>', | ||
' <div id="gra-tabs">', | ' <div id="gra-tabs">', | ||
' <button class="gra-tab gra-tab-active" id="gra-tab-comments">', | ' <button class="gra-tab gra-tab-active" id="gra-tab-comments">', | ||
' <span class="gra-icon gra-icon-comment"></span> | ' <span class="gra-icon gra-icon-comment" aria-hidden="true"></span> टिप्पण्यः', | ||
' </button>', | ' </button>', | ||
' <button class="gra-tab" id="gra-tab-bookmarks">', | ' <button class="gra-tab" id="gra-tab-bookmarks">', | ||
' <span class="gra-icon gra-icon-bookmark"></span> | ' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span> चिह्नाः', | ||
' </button>', | ' </button>', | ||
' </div>', | ' </div>', | ||
| Line 199: | Line 197: | ||
$( 'body' ).append( $panel ); | $( 'body' ).append( $panel ); | ||
$backdrop = $( '<div id="gra-backdrop" aria-hidden="true"></div>' ); | |||
$backdrop = $('<div id="gra-backdrop"></div>'); | |||
$( 'body' ).append( $backdrop ); | $( 'body' ).append( $backdrop ); | ||
// | // Toggle FAB (bottom-right, persistent) | ||
var $toggle = $( [ | var $toggle = $( [ | ||
'<button id="gra-toggle" | '<button id="gra-toggle" aria-label="टिप्पणियाँ">', | ||
' <span class="gra-icon gra-icon-comment" id="gra-toggle-icon"></span>', | ' <span class="gra-icon gra-icon-comment" id="gra-toggle-icon" aria-hidden="true"></span>', | ||
' <span id="gra-toggle-badge"></span>', | ' <span id="gra-toggle-badge" aria-live="polite"></span>', | ||
'</button>', | '</button>', | ||
].join('') ); | ].join('') ); | ||
$( 'body' ).append( $toggle ); | $( 'body' ).append( $toggle ); | ||
$toggle.on( 'click', function() { | $toggle.on( 'click', function() { | ||
$panel.hasClass('gra-panel-open') ? closePanel() : openPanel( _activeTab ); | |||
} ); | } ); | ||
// Cache | // Cache | ||
$( '#gra-panel-title' ) | |||
.text( pageTitle.replace(/_/g,' ').split('/')[0].slice(0,30) ); | |||
$tabComments = $( '#gra-tab-comments' ); | |||
.text( pageTitle.replace(/_/g,' ').split('/')[0].slice(0,30) | $tabBookmarks = $( '#gra-tab-bookmarks' ); | ||
$paneComments = $( '#gra-pane-comments' ); | |||
$tabComments | $paneBookmarks= $( '#gra-pane-bookmarks' ); | ||
$tabBookmarks | $cmpInput = $( '#gra-cmp-input' ); | ||
$paneComments | $cmpSubmit = $( '#gra-cmp-submit' ); | ||
$paneBookmarks = $( '#gra-pane-bookmarks' ); | $cmpName = $( '#gra-cmp-name' ); // may be empty set for logged-in users | ||
$cmpInput | $bmInput = $( '#gra-bm-input' ); | ||
$cmpSubmit | $bmSubmit = $( '#gra-bm-submit' ); | ||
$ | |||
$ | |||
} | } | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// SELECTION | // SELECTION — desktop + mobile | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
| Line 278: | Line 233: | ||
var sel = window.getSelection(); | var sel = window.getSelection(); | ||
if ( !sel || sel.isCollapsed || !sel.rangeCount ) return false; | if ( !sel || sel.isCollapsed || !sel.rangeCount ) return false; | ||
var range = sel.getRangeAt(0); | |||
var range | var text = sel.toString().trim(); | ||
var text | |||
if ( !text || text.length < 2 ) return false; | if ( !text || text.length < 2 ) return false; | ||
var contentEl = document.querySelector( CONTENT_SEL ); | var contentEl = document.querySelector( CONTENT_SEL ); | ||
if ( !contentEl ) return false; | if ( !contentEl ) return false; | ||
var | var start = range.commonAncestorContainer; | ||
if ( | if ( start.nodeType === 3 ) start = start.parentNode; | ||
if ( !contentEl.contains( | if ( !contentEl.contains( start ) ) return false; | ||
_selText = text; | _selText = text; | ||
| Line 294: | Line 247: | ||
_selRect = range.getBoundingClientRect(); | _selRect = range.getBoundingClientRect(); | ||
return true; | return true; | ||
} | |||
function tryShowFab() { | |||
if ( $cmpComposer.hasClass('gra-composer-visible') ) return; | |||
if ( $bmComposer.hasClass('gra-composer-visible') ) return; | |||
if ( captureSelection() ) { | |||
_fabSelVer = _selVersion; | |||
showFab( _selRect ); | |||
} else { | |||
hideFab(); | |||
} | |||
} | } | ||
// ════════════════════════════════════════════════════════════════════ | |||
// FAB POSITIONING | |||
// ════════════════════════════════════════════════════════════════════ | |||
function showFab( rect ) { | |||
if ( !rect ) return; | |||
var fabW = 46, fabH = 84; | |||
var top = rect.top + ( rect.height / 2 ) - ( fabH / 2 ); | |||
var left = rect.right + 10; | |||
if ( left + fabW > window.innerWidth - 8 ) left = rect.left - fabW - 10; | |||
top = clamp( top, 8, window.innerHeight - fabH - 8 ); | |||
left = clamp( left, 8, window.innerWidth - fabW - 8 ); | |||
$fab.css({ top: top + 'px', left: left + 'px' }).addClass('gra-fab-visible'); | |||
} | |||
function hideFab() { $fab.removeClass('gra-fab-visible'); } | |||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
| Line 302: | Line 283: | ||
function positionComposer( $el ) { | function positionComposer( $el ) { | ||
if ( !_selRect ) return; | if ( !_selRect ) return; | ||
var W = 308; | |||
var top = _selRect.bottom + 8; | var top = _selRect.bottom + 8; | ||
var left = _selRect.left; | var left = _selRect.left; | ||
if ( left + W > window.innerWidth - 8 ) left = window.innerWidth - W - 8; | |||
if ( left + | |||
left = Math.max( left, 8 ); | left = Math.max( left, 8 ); | ||
if ( top + 200 > window.innerHeight ) top = _selRect.top - 210; | |||
if ( top + | top = Math.max( top, 8 ); | ||
// On very small screens center it | |||
if ( window.innerWidth < 400 ) left = ( window.innerWidth - W ) / 2; | |||
top = Math.max( top, 8 ); | |||
$el.css({ top: top + 'px', left: left + 'px' }); | $el.css({ top: top + 'px', left: left + 'px' }); | ||
} | } | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// WRAP SELECTION | // WRAP SELECTION | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
function wrapSelection( id, cssClass ) { | function wrapSelection( id, cssClass ) { | ||
if ( !_selRange ) return null; | if ( !_selRange ) return null; | ||
var range = _selRange; | var range = _selRange; | ||
_selRange = null; | _selRange = null; | ||
| Line 336: | Line 309: | ||
range.surroundContents( span ); | range.surroundContents( span ); | ||
return span; | return span; | ||
} catch ( e ) { | } catch (e) { | ||
try { | try { | ||
var frag = range.extractContents(); | var frag = range.extractContents(); | ||
var | var sp2 = document.createElement('span'); | ||
sp2.className = cssClass; | |||
sp2.setAttribute('data-gra-id', id); | |||
sp2.appendChild(frag); | |||
range.insertNode( | range.insertNode(sp2); | ||
return | return sp2; | ||
} catch(e2) { return null; } | } catch(e2) { return null; } | ||
} | } | ||
| Line 357: | Line 328: | ||
function openCommentComposer() { | function openCommentComposer() { | ||
hideFab(); | hideFab(); | ||
positionComposer( $cmpComposer ); | positionComposer( $cmpComposer ); | ||
$cmpComposer.addClass('gra-composer-visible'); | $cmpComposer.addClass('gra-composer-visible'); | ||
$cmpInput. | // On mobile, delay focus so keyboard doesn't displace the composer | ||
setTimeout( function() { $cmpInput.focus(); }, isMobile() ? 300 : 0 ); | |||
} | } | ||
| Line 366: | Line 337: | ||
$cmpComposer.removeClass('gra-composer-visible'); | $cmpComposer.removeClass('gra-composer-visible'); | ||
$cmpInput.val(''); | $cmpInput.val(''); | ||
if ( $cmpName.length ) $cmpName.val(''); | |||
$cmpSubmit.prop('disabled', true); | $cmpSubmit.prop('disabled', true); | ||
_selRange = null; | _selRange = null; _selText = ''; _selRect = null; | ||
} | } | ||
function submitComment() { | function submitComment() { | ||
var text | var text = $cmpInput.val().trim(); | ||
if ( !text || | if ( !text ) return; | ||
// Resolve author name: logged-in user or anonymous name or "अतिथि" | |||
var author = currentUser | |||
|| ( $cmpName.length ? $cmpName.val().trim() : '' ) | |||
|| 'अतिथि'; | |||
var id = uid(); | var id = uid(); | ||
var ts = nowIso(); | var ts = nowIso(); | ||
var quote = _selText.slice(0, 120) + ( _selText.length > 120 ? '…' : '' ); | var quote = _selText.slice(0,120) + (_selText.length > 120 ? '…' : ''); | ||
var span = wrapSelection( id, 'gra-comment-highlight' ); | var span = wrapSelection( id, 'gra-comment-highlight' ); | ||
if ( span ) span.setAttribute('data-gra-quote', quote); | if ( span ) span.setAttribute('data-gra-quote', quote); | ||
var entry = { id:id, author:author, ts:ts, quote:quote, text:text }; | |||
var entry = { id:id, author: | |||
_comments.push( entry ); | _comments.push( entry ); | ||
persistCommentHighlight( id, quote ); | persistCommentHighlight( id, quote ); | ||
saveCommentToWiki( id, author, quote, text, ts ); | |||
notifyByEmail( id, author, quote, text, ts ); | |||
renderCommentCards(); | renderCommentCards(); | ||
closeCommentComposer(); | closeCommentComposer(); | ||
| Line 402: | Line 369: | ||
} | } | ||
function saveCommentToWiki( id, quote, text, ts ) { | function saveCommentToWiki( id, author, quote, text, ts ) { | ||
if ( !window.mw ) return; | if ( !window.mw ) return; | ||
var api = new mw.Api(); | var api = new mw.Api(); | ||
| Line 409: | Line 376: | ||
rvprop:'content', rvslots:'main', formatversion:2, | rvprop:'content', rvslots:'main', formatversion:2, | ||
}).then(function(data){ | }).then(function(data){ | ||
var page = (data.query.pages||[])[0]||{}; | var page = ((data.query && data.query.pages)||[])[0]||{}; | ||
var existing = ''; | var existing = ''; | ||
if (page.revisions && page.revisions[0]) { | if (page.revisions && page.revisions[0]) { | ||
| Line 416: | Line 383: | ||
} | } | ||
var entry = '{{GrComment\n' | var entry = '{{GrComment\n' | ||
+ '| id = ' + id | + '| id = ' + id + '\n' | ||
+ '| author = ' + | + '| author = ' + author + '\n' | ||
+ '| timestamp = ' + ts | + '| timestamp = ' + ts + '\n' | ||
+ '| quote = ' + quote.replace(/\n/g,' ') + '\n' | + '| quote = ' + quote.replace(/\n/g,' ') + '\n' | ||
+ '| text = ' + text.replace(/\n/g,' ') + '\n' | + '| text = ' + text.replace(/\n/g,' ') + '\n' | ||
| Line 425: | Line 392: | ||
api.postWithEditToken({ | api.postWithEditToken({ | ||
action:'edit', title:commentsPage, text:updated, | action:'edit', title:commentsPage, text:updated, | ||
summary:' | summary:'टिप्पणी — ' + author, | ||
bot:0, | bot:0, | ||
}).catch(function(){}); | }).catch(function(){}); | ||
| Line 431: | Line 398: | ||
} | } | ||
function | function notifyByEmail( anchorId, author, quote, text, ts ) { | ||
var artPath = (window.mw ? mw.config.get('wgArticlePath') : '/wiki/$1') || '/wiki/$1'; | |||
var anchorLink = window.location.origin | |||
+ artPath.replace('$1', pageTitle) | |||
+ (anchorId ? '#' + anchorId : ''); | |||
var pageDisplay = pageTitle.replace(/_/g,' '); | |||
var subject = encodeURIComponent('[Grantha] टिप्पणी — "' + pageDisplay + '"'); | |||
var body = encodeURIComponent( | |||
var | 'Page : ' + pageDisplay + '\n' | ||
+ 'Author : ' + author + '\n' | |||
+ 'Time : ' + ts + '\n' | |||
+ 'Passage : "' + quote + '"\n\n' | |||
+ 'Comment :\n' + text + '\n\n' | |||
+ '──────────────────────────────────\n' | |||
+ 'Link: ' + anchorLink + '\n' | |||
+ 'Comments page: ' + window.location.origin | |||
+ artPath.replace('$1', 'Talk:' + pageTitle + '/GrComments') | |||
); | |||
// Always notify via mailto to feedback@anandamakaranda.in | |||
var | var a = document.createElement('a'); | ||
a.href = 'mailto:' + FEEDBACK_EMAIL + '?subject=' + subject + '&body=' + body; | |||
a.target = '_blank'; | |||
a.rel = 'noopener'; | |||
document.body.appendChild(a); | |||
a.click(); | |||
document.body.removeChild(a); | |||
} | } | ||
// ── Load comments | // ── Load + parse comments ──────────────────────────────────────────── | ||
function loadComments( cb ) { | function loadComments( cb ) { | ||
if ( _cmtLoaded ) { if (cb) cb(); return; } | if ( _cmtLoaded ) { if (cb) cb(); return; } | ||
if ( !window.mw ) { _cmtLoaded=true; if(cb)cb(); return; } | if ( !window.mw ) { _cmtLoaded = true; if (cb) cb(); return; } | ||
new mw.Api().get({ | new mw.Api().get({ | ||
action:'query', prop:'revisions', titles:commentsPage, | action:'query', prop:'revisions', titles:commentsPage, | ||
rvprop:'content', rvslots:'main', formatversion:2, | rvprop:'content', rvslots:'main', formatversion:2, | ||
}).then(function(data){ | }).then(function(data){ | ||
var page = (data.query.pages||[])[0]||{}; | var page = ((data.query && data.query.pages)||[])[0]||{}; | ||
var wt = ''; | var wt = ''; | ||
if (page.revisions && page.revisions[0]) { | if (page.revisions && page.revisions[0]) { | ||
| Line 508: | Line 445: | ||
_cmtLoaded = true; | _cmtLoaded = true; | ||
if (cb) cb(); | if (cb) cb(); | ||
}).catch(function(){ _cmtLoaded=true; if(cb)cb(); }); | }).catch(function(){ _cmtLoaded = true; if (cb) cb(); }); | ||
} | } | ||
function parseCommentsWt( wt ) { | function parseCommentsWt( wt ) { | ||
var out = [] | var out = [], re = /\{\{GrComment\s*\n([\s\S]*?)\}\}/g, m; | ||
while ((m = re.exec(wt)) !== null) { | while ((m = re.exec(wt)) !== null) { | ||
var block = m[1] | var block = m[1], f = {}, lr = /\|\s*([\w_]+)\s*=\s*([\s\S]*?)(?=\n\||\n\}\}|$)/g, lm; | ||
while ((lm = lr.exec(block)) !== null) f[lm[1].trim()] = lm[2].trim(); | |||
if (f.id) out.push({ id:f.id, author:f.author||'अतिथि', ts:f.timestamp||'', quote:f.quote||'', text:f.text||'' }); | |||
while ((lm = lr.exec(block)) !== null) | |||
if (f.id) out.push({ | |||
} | } | ||
return out; | return out; | ||
| Line 539: | Line 466: | ||
positionComposer( $bmComposer ); | positionComposer( $bmComposer ); | ||
$bmComposer.addClass('gra-composer-visible'); | $bmComposer.addClass('gra-composer-visible'); | ||
$bmInput. | setTimeout( function() { $bmInput.focus(); }, isMobile() ? 300 : 0 ); | ||
} | } | ||
| Line 545: | Line 472: | ||
$bmComposer.removeClass('gra-composer-visible'); | $bmComposer.removeClass('gra-composer-visible'); | ||
$bmInput.val(''); | $bmInput.val(''); | ||
_selRange = null; | _selRange = null; _selText = ''; _selRect = null; | ||
} | } | ||
function submitBookmark() { | function submitBookmark() { | ||
var name = $bmInput.val().trim() || ( ' | var name = $bmInput.val().trim() || ('चिह्न ' + (_bookmarks.length + 1)); | ||
var id = uid(); | var id = uid(); | ||
var quote = _selText.slice(0,120) + (_selText.length > 120 ? '…' : ''); | |||
var quote = _selText.slice(0,120) + (_selText.length>120?'…':''); | var span = wrapSelection(id, 'gra-bookmark-highlight'); | ||
if (span) { span.setAttribute('data-gra-id', id); span.setAttribute('data-gra-name', name); } | |||
_bookmarks.push({ id:id, name:name, quote:quote, ts:nowIso() }); | |||
var span = wrapSelection( id, 'gra-bookmark-highlight' ); | |||
if ( span ) { | |||
persistBookmarks(); | persistBookmarks(); | ||
renderBookmarkCards(); | renderBookmarkCards(); | ||
| Line 573: | Line 490: | ||
function deleteBookmark( id ) { | function deleteBookmark( id ) { | ||
_bookmarks = _bookmarks.filter(function(b){ return b.id !== id; }); | _bookmarks = _bookmarks.filter(function(b){ return b.id !== id; }); | ||
var span = document.querySelector('[data-gra-id="'+id+'"].gra-bookmark-highlight'); | |||
var span = document.querySelector('[data-gra-id="' + id + '"].gra-bookmark-highlight'); | |||
if (span) { | if (span) { | ||
var parent = span.parentNode; | var parent = span.parentNode; | ||
| Line 580: | Line 496: | ||
parent.removeChild(span); | parent.removeChild(span); | ||
} | } | ||
persistBookmarks(); | persistBookmarks(); renderBookmarkCards(); | ||
} | } | ||
| Line 587: | Line 502: | ||
try { localStorage.setItem(BM_LS_KEY, JSON.stringify(_bookmarks)); } catch(e){} | try { localStorage.setItem(BM_LS_KEY, JSON.stringify(_bookmarks)); } catch(e){} | ||
} | } | ||
function loadBookmarks() { | function loadBookmarks() { | ||
try { | try { var r = localStorage.getItem(BM_LS_KEY); if (r) _bookmarks = JSON.parse(r)||[]; } catch(e){} | ||
} | } | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// PANEL | // PANEL | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
function openPanel( tab ) { | function openPanel(tab) { | ||
_activeTab = tab || _activeTab; | _activeTab = tab || _activeTab; | ||
switchTab( _activeTab ); | switchTab(_activeTab); | ||
$panel.addClass('gra-panel-open'); | $panel.addClass('gra-panel-open'); | ||
$backdrop.addClass('gra-backdrop-visible'); | $backdrop.addClass('gra-backdrop-visible'); | ||
} | } | ||
function closePanel() { | function closePanel() { | ||
$panel.removeClass('gra-panel-open'); | $panel.removeClass('gra-panel-open'); | ||
$backdrop.removeClass('gra-backdrop-visible'); | $backdrop.removeClass('gra-backdrop-visible'); | ||
} | } | ||
function switchTab(tab) { | |||
function switchTab( tab ) { | |||
_activeTab = tab; | _activeTab = tab; | ||
$tabComments.toggleClass('gra-tab-active', tab==='comments'); | $tabComments.toggleClass('gra-tab-active', tab==='comments'); | ||
| Line 617: | Line 526: | ||
$paneComments.toggleClass('gra-pane-active', tab==='comments'); | $paneComments.toggleClass('gra-pane-active', tab==='comments'); | ||
$paneBookmarks.toggleClass('gra-pane-active', tab==='bookmarks'); | $paneBookmarks.toggleClass('gra-pane-active', tab==='bookmarks'); | ||
if (tab==='comments') | if (tab==='comments') loadComments(function(){ renderCommentCards(); }); | ||
else renderBookmarkCards(); | |||
} | } | ||
| Line 629: | Line 535: | ||
function renderCommentCards() { | function renderCommentCards() { | ||
if ( _comments.length | if (!_comments.length) { | ||
$paneComments.html('<div class="gra-empty-state"> | $paneComments.html('<div class="gra-empty-state">अभी कोई टिप्पणी नहीं।<br>पाठ चुनें और 💬 पर क्लिक करें।</div>'); | ||
return; | return; | ||
} | } | ||
| Line 641: | Line 547: | ||
+ '<div class="gra-card-author">' + esc(c.author) + '</div>' | + '<div class="gra-card-author">' + esc(c.author) + '</div>' | ||
+ (c.ts ? '<div class="gra-card-ts">' + esc(fmtTs(c.ts)) + '</div>' : '') | + (c.ts ? '<div class="gra-card-ts">' + esc(fmtTs(c.ts)) + '</div>' : '') | ||
+ '</div> | + '</div></div>' | ||
+ (c.quote ? '<div class="gra-card-quote">' + esc(c.quote) + '</div>' : '') | + (c.quote ? '<div class="gra-card-quote">' + esc(c.quote) + '</div>' : '') | ||
+ '<div class="gra-card-text">' + esc(c.text) + '</div>' | + '<div class="gra-card-text">' + esc(c.text) + '</div>' | ||
| Line 651: | Line 556: | ||
function renderBookmarkCards() { | function renderBookmarkCards() { | ||
if ( _bookmarks.length | if (!_bookmarks.length) { | ||
$paneBookmarks.html('<div class="gra-empty-state"> | $paneBookmarks.html('<div class="gra-empty-state">अभी कोई चिह्न नहीं।<br>पाठ चुनें और 🔖 पर क्लिक करें।</div>'); | ||
return; | return; | ||
} | } | ||
| Line 658: | Line 563: | ||
_bookmarks.slice().reverse().forEach(function(b){ | _bookmarks.slice().reverse().forEach(function(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"></span>' | + '<span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>' | ||
+ '<div class="gra-bookmark-info">' | + '<div class="gra-bookmark-info">' | ||
+ '<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>' | + '</div>' | ||
+ '<button class="gra-bookmark-del" data-del-id="' + esc(b.id) + '" title=" | + '<button class="gra-bookmark-del" data-del-id="' + esc(b.id) + '" title="हटाएं">×</button>' | ||
+ '</div>'; | + '</div>'; | ||
}); | }); | ||
| Line 673: | Line 578: | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
function scrollToHighlight( id ) { | function scrollToHighlight(id) { | ||
var el = document.querySelector('[data-gra-id="' + id + '"]'); | var el = document.querySelector('[data-gra-id="' + id + '"]'); | ||
if (!el) return; | if (!el) return; | ||
| Line 682: | Line 587: | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// EVENT WIRING | // EVENT WIRING (desktop + mobile) | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
function wireEvents() { | function wireEvents() { | ||
// ── | // ── Desktop: mouseup / keyup ────────────────────────────────── | ||
$( document ).on('mouseup keyup', function(e){ | $( document ).on('mouseup keyup', function(e){ | ||
// | if ( e.type === 'mouseup' && e.button !== 0 ) return; // left-click only | ||
setTimeout( | setTimeout( tryShowFab, 20 ); | ||
}); | |||
// ── Mobile: selectionchange (iOS + Android) ─────────────────── | |||
// 'selectionchange' fires continuously while user drags selection handles. | |||
// We debounce it and only act after the user has stopped for 400ms. | |||
var _selChangeTimer = null; | |||
document.addEventListener('selectionchange', function() { | |||
}, | _selVersion++; | ||
clearTimeout( _selChangeTimer ); | |||
var v = _selVersion; | |||
_selChangeTimer = setTimeout(function(){ | |||
// Only trigger if this is still the latest selectionchange | |||
if ( v !== _selVersion ) return; | |||
if ( _fabSelVer === v ) return; // already showed FAB for this selection | |||
tryShowFab(); | |||
}, 400); | |||
}); | }); | ||
// ── Mobile: touchend fallback (catch tap-to-end-selection) ──── | |||
document.addEventListener('touchend', function(e){ | |||
// Don't fire if tapping the FAB itself | |||
if ( $fab[0] && $fab[0].contains(e.target) ) return; | |||
if ( $cmpComposer[0] && $cmpComposer[0].contains(e.target) ) return; | |||
if ( $bmComposer[0] && $bmComposer[0].contains(e.target) ) return; | |||
// Small delay — browser settles selection after touchend | |||
setTimeout( tryShowFab, 80 ); | |||
}, { passive: true }); | |||
// ── Click outside → hide FAB ────────────────────────────────── | // ── Click outside → hide FAB ────────────────────────────────── | ||
$( document ).on('mousedown', function(e){ | $( document ).on('mousedown touchstart', function(e){ | ||
var | var t = e.target; | ||
if ( | if ( $fab[0] && $fab[0].contains(t) ) return; | ||
if ( $cmpComposer[0] && $cmpComposer[0].contains(t) ) return; | |||
if ( $bmComposer[0] && $bmComposer[0].contains(t) ) return; | |||
hideFab(); | |||
}); | }); | ||
// ── FAB | // ── FAB buttons ─────────────────────────────────────────────── | ||
$( '#gra-fab-comment' ).on('click', function(e){ | $( '#gra-fab-comment' ).on('click touchend', function(e){ | ||
e.preventDefault(); | e.preventDefault(); e.stopPropagation(); | ||
// Re-capture in case selection was cleared by the tap | |||
if ( !captureSelection() ) return; | if ( !_selRange && !captureSelection() ) return; | ||
openCommentComposer(); | openCommentComposer(); | ||
}); | }); | ||
$( '#gra-fab-bookmark' ).on('click touchend', function(e){ | |||
e.preventDefault(); e.stopPropagation(); | |||
$( '#gra-fab-bookmark' ).on('click', function(e){ | if ( !_selRange && !captureSelection() ) return; | ||
e.preventDefault(); | |||
if ( !captureSelection() ) return; | |||
openBookmarkComposer(); | openBookmarkComposer(); | ||
}); | }); | ||
| Line 729: | Line 648: | ||
// ── Comment composer ────────────────────────────────────────── | // ── Comment composer ────────────────────────────────────────── | ||
$cmpInput.on('input', function(){ | $cmpInput.on('input', function(){ | ||
$cmpSubmit.prop('disabled', !$(this).val().trim() | $cmpSubmit.prop('disabled', !$( this ).val().trim()); | ||
}); | }); | ||
$( '#gra-cmp-cancel' ).on('click', function(){ closeCommentComposer(); hideFab(); }); | |||
$cmpSubmit.on('click', submitComment); | $cmpSubmit.on('click', submitComment); | ||
$cmpInput.on('keydown', function(e){ | $cmpInput.on('keydown', function(e){ | ||
if ((e.ctrlKey||e.metaKey) && e.key==='Enter') submitComment(); | |||
if ((e.ctrlKey||e.metaKey) && e.key==='Enter') | |||
if (e.key==='Escape') { closeCommentComposer(); hideFab(); } | if (e.key==='Escape') { closeCommentComposer(); hideFab(); } | ||
}); | }); | ||
// ── Bookmark composer ───────────────────────────────────────── | // ── Bookmark composer ───────────────────────────────────────── | ||
$( '#gra-bm-cancel' ).on('click', function(){ | $( '#gra-bm-cancel' ).on('click', function(){ closeBookmarkComposer(); hideFab(); }); | ||
$bmSubmit.on('click', submitBookmark); | $bmSubmit.on('click', submitBookmark); | ||
$bmInput.on('keydown', function(e){ | $bmInput.on('keydown', function(e){ | ||
if (e.key==='Enter') submitBookmark(); | if (e.key==='Enter') submitBookmark(); | ||
if (e.key==='Escape'){ closeBookmarkComposer(); hideFab(); } | if (e.key==='Escape') { closeBookmarkComposer(); hideFab(); } | ||
}); | }); | ||
// ── Panel | // ── Panel ───────────────────────────────────────────────────── | ||
$( '#gra-panel-close' ).on('click', closePanel); | $( '#gra-panel-close' ).on('click', closePanel); | ||
$backdrop.on('click', closePanel); | $backdrop.on('click touchstart', closePanel); | ||
$tabComments.on('click', function(){ switchTab('comments'); }); | $tabComments.on('click', function(){ switchTab('comments'); }); | ||
$tabBookmarks.on('click', function(){ switchTab('bookmarks'); }); | $tabBookmarks.on('click', function(){ switchTab('bookmarks'); }); | ||
// | // Click comment card → scroll to highlight | ||
$paneComments.on('click', '.gra-comment-card', function(){ | $paneComments.on('click', '.gra-comment-card', function(){ | ||
var id = $(this).attr('data-gra-id'); | var id = $( this ).attr('data-gra-id'); | ||
if (id) scrollToHighlight(id); | if (id) { closePanel(); scrollToHighlight(id); } | ||
}); | }); | ||
// Click bookmark card → scroll | |||
// | |||
$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; | ||
var id = $(this).attr('data-gra-id'); | var id = $( this ).attr('data-gra-id'); | ||
if (id) scrollToHighlight(id); | if (id) { closePanel(); scrollToHighlight(id); } | ||
}); | }); | ||
// Delete bookmark | |||
// | |||
$paneBookmarks.on('click', '.gra-bookmark-del', function(e){ | $paneBookmarks.on('click', '.gra-bookmark-del', function(e){ | ||
e.stopPropagation(); | e.stopPropagation(); | ||
var id = $(this).attr('data-del-id'); | var id = $( this ).attr('data-del-id'); | ||
if (id) deleteBookmark(id); | if (id) deleteBookmark(id); | ||
}); | }); | ||
// | // Highlight in text → open panel | ||
$( CONTENT_SEL ).on('click', '.gra-comment-highlight', function(){ | $( CONTENT_SEL ).on('click', '.gra-comment-highlight', function(){ | ||
var id = $(this).attr('data-gra-id'); | var id = $( this ).attr('data-gra-id'); | ||
openPanel('comments'); | openPanel('comments'); | ||
setTimeout(function(){ | setTimeout(function(){ | ||
var $card = $paneComments.find('[data-gra-id="'+id+'"]'); | var $card = $paneComments.find('[data-gra-id="'+id+'"]'); | ||
if ($card.length) { | if ($card.length) { | ||
$card.addClass('gra-card-active'); | $card.addClass('gra-card-active'); | ||
$card[0].scrollIntoView({behavior:'smooth',block:'nearest'}); | $card[0].scrollIntoView({behavior:'smooth', block:'nearest'}); | ||
setTimeout(function(){ $card.removeClass('gra-card-active'); }, 2000); | setTimeout(function(){ $card.removeClass('gra-card-active'); }, 2000); | ||
} | } | ||
}, 100); | }, 100); | ||
}); | }); | ||
$( CONTENT_SEL ).on('click', '.gra-bookmark-highlight', function(){ | $( CONTENT_SEL ).on('click', '.gra-bookmark-highlight', function(){ | ||
var id = $(this).attr('data-gra-id'); | var id = $( this ).attr('data-gra-id'); | ||
openPanel('bookmarks'); | openPanel('bookmarks'); | ||
setTimeout(function(){ | setTimeout(function(){ | ||
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); | }, 100); | ||
}); | }); | ||
// | // Escape | ||
$( document ).on('keydown', function(e){ | $( document ).on('keydown', function(e){ | ||
if (e.key | if (e.key !== 'Escape') return; | ||
if ($cmpComposer.hasClass('gra-composer-visible')) { closeCommentComposer(); hideFab(); } | |||
else if ($bmComposer.hasClass('gra-composer-visible')) { closeBookmarkComposer(); hideFab(); } | |||
else closePanel(); | |||
}); | }); | ||
} | } | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// RESTORE | // RESTORE HIGHLIGHTS on page reload | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
function persistCommentHighlight(id, quote) { | |||
function persistCommentHighlight( id, quote ) { | |||
try { | try { | ||
var | var s = JSON.parse(localStorage.getItem(CMT_LS_KEY)||'[]'); | ||
s = s.filter(function(h){ return h.id !== id; }); | |||
s.push({id:id, quote:quote}); | |||
localStorage.setItem(CMT_LS_KEY, JSON.stringify(s)); | |||
localStorage.setItem( CMT_LS_KEY, JSON.stringify( | |||
} catch(e){} | } catch(e){} | ||
} | } | ||
function restoreCommentHighlights() { | function restoreCommentHighlights() { | ||
var | var s = []; | ||
try { | try { s = JSON.parse(localStorage.getItem(CMT_LS_KEY)||'[]'); } catch(e){} | ||
s.forEach(function(h){ | |||
if ( !h.quote || !h.id ) return; | if (!h.quote || !h.id) return; | ||
if ( document.querySelector( '[data-gra-id="' + h.id + '"].gra-comment-highlight' ) ) return; | if (document.querySelector('[data-gra-id="'+h.id+'"].gra-comment-highlight')) return; | ||
var needle = h.quote.replace(/…$/, '').trim().slice(0, 80); | var needle = h.quote.replace(/…$/,'').trim().slice(0,80); | ||
if ( !needle ) return; | if (!needle) return; | ||
var range = findTextInContent( document.querySelector(CONTENT_SEL), needle ); | var range = findTextInContent(document.querySelector(CONTENT_SEL), needle); | ||
if ( range ) { | if (range) { | ||
var | var sp = document.createElement('span'); | ||
sp.className = 'gra-comment-highlight'; | |||
sp.setAttribute('data-gra-id', h.id); | |||
try { range.surroundContents( | try { range.surroundContents(sp); } catch(e){} | ||
} | } | ||
}); | }); | ||
| Line 857: | Line 753: | ||
function restoreBookmarkHighlights() { | function restoreBookmarkHighlights() { | ||
_bookmarks.forEach(function(b){ | _bookmarks.forEach(function(b){ | ||
if ( !b.quote ) return; | if (!b.quote) return; | ||
if (document.querySelector('[data-gra-id="'+b.id+'"].gra-bookmark-highlight')) return; | |||
if ( document.querySelector('[data-gra-id="'+b.id+'"].gra-bookmark-highlight') | |||
var needle = b.quote.replace(/…$/,'').trim().slice(0,60); | var needle = b.quote.replace(/…$/,'').trim().slice(0,60); | ||
if (!needle) return; | if (!needle) return; | ||
var found = findTextInContent(document.querySelector(CONTENT_SEL), needle); | |||
var found = findTextInContent( | |||
if (found) { | if (found) { | ||
var | var sp = document.createElement('span'); | ||
sp.className = 'gra-bookmark-highlight'; | |||
sp.setAttribute('data-gra-id', b.id); | |||
sp.setAttribute('data-gra-name', b.name); | |||
try { | try { found.surroundContents(sp); } catch(e){} | ||
} | } | ||
}); | }); | ||
} | } | ||
function findTextInContent( root, needle ) { | function findTextInContent(root, needle) { | ||
if (!root) return null; | |||
var text = root.textContent || ''; | var text = root.textContent || ''; | ||
var idx = text.indexOf(needle); | var idx = text.indexOf(needle); | ||
if (idx < 0) return null; | if (idx < 0) return null; | ||
var iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, null, false); | var iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, null, false); | ||
var pos | var pos = 0, node, startNode, startOffset, endNode, endOffset; | ||
while ((node = iter.nextNode())) { | while ((node = iter.nextNode())) { | ||
var len = node.nodeValue.length; | var len = node.nodeValue.length; | ||
if (!startNode && pos+len > idx) { | if (!startNode && pos + len > idx) { startNode = node; startOffset = idx - pos; } | ||
var endIdx = idx + needle.length; | var endIdx = idx + needle.length; | ||
if (startNode && pos+len >= endIdx) { | if (startNode && pos + len >= endIdx) { endNode = node; endOffset = endIdx - pos; break; } | ||
pos += len; | pos += len; | ||
} | } | ||
if (!startNode || !endNode) return null; | if (!startNode || !endNode) return null; | ||
var | var r = document.createRange(); | ||
r.setStart(startNode, startOffset); | |||
r.setEnd(endNode, endOffset); | |||
return | return r; | ||
} | } | ||
| Line 919: | Line 799: | ||
restoreBookmarkHighlights(); | restoreBookmarkHighlights(); | ||
restoreCommentHighlights(); | restoreCommentHighlights(); | ||
loadComments(function(){ | loadComments(function(){ | ||
if (_comments.length > 0) { | if (_comments.length > 0) { | ||
$tabComments.find(' | $tabComments.find('.gra-icon').after( | ||
' <span style="background:#e53935;color:#fff;border-radius:9px; | ' <span style="background:#e53935;color:#fff;border-radius:9px;font-size:10px;padding:0 5px;margin-left:2px;">' | ||
+ _comments.length + '</span>' | |||
); | ); | ||
var $badge = $( '#gra-toggle-badge' ); | var $badge = $( '#gra-toggle-badge' ); | ||
if ( $badge.length ) $badge.text( _comments.length ).css('display','flex'); | if ($badge.length) $badge.text(_comments.length).css('display','flex'); | ||
} | } | ||
}); | }); | ||
} ); | }); | ||
}() ); | }() ); | ||