MediaWiki:Gadget-GrAnnotations.js: Difference between revisions
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
/** | /** | ||
* gr_annotations.js — grantha.io inline Notes + Bookmarks + Feedback ( | * gr_annotations.js — grantha.io inline Notes + Bookmarks + Feedback (v5-patch1) | ||
* ═════════════════════════════════════════════════════════════════════ | |||
* | |||
*/ | */ | ||
| Line 43: | Line 32: | ||
// ── State ──────────────────────────────────────────────────────────── | // ── State ──────────────────────────────────────────────────────────── | ||
var _selRange = null; | var _selRange = null; // cloned Range — kept alive across taps | ||
var _selText = ''; | var _selText = ''; | ||
var _selRect = null; | var _selRect = null; | ||
| Line 51: | Line 40: | ||
var _selVersion = 0; | var _selVersion = 0; | ||
var _fabSelVer = -1; | var _fabSelVer = -1; | ||
var _mobile = window.innerWidth < 768 || 'ontouchstart' in window; | var _mobile = window.innerWidth < 768 || 'ontouchstart' in window; | ||
// ── Helpers ────────────────────────────────────────────────────────── | // ── Helpers ────────────────────────────────────────────────────────── | ||
| Line 82: | Line 71: | ||
function buildDom() { | function buildDom() { | ||
$fab = $( [ | $fab = $( [ | ||
'<div id="gra-fab" role="toolbar" aria-label="Feedback / | '<div id="gra-fab" role="toolbar" aria-label="Feedback / Notes / Bookmark">', | ||
' <button class="gra-fab-btn" id="gra-fab-feedback" type="button" aria-label="Send feedback">', | ' <button class="gra-fab-btn" id="gra-fab-feedback" type="button" aria-label="Send feedback">', | ||
' <span class="gra-icon gra-icon-feedback" aria-hidden="true"></span>', | ' <span class="gra-icon gra-icon-feedback" aria-hidden="true"></span>', | ||
| Line 91: | Line 79: | ||
' <button class="gra-fab-btn" id="gra-fab-note" type="button" aria-label="Add note">', | ' <button class="gra-fab-btn" id="gra-fab-note" type="button" aria-label="Add note">', | ||
' <span class="gra-icon gra-icon-note" aria-hidden="true"></span>', | ' <span class="gra-icon gra-icon-note" aria-hidden="true"></span>', | ||
' <span class="gra-fab-tooltip"> | ' <span class="gra-fab-tooltip">Notes</span>', | ||
' </button>', | ' </button>', | ||
' <button class="gra-fab-btn" id="gra-fab-bookmark" type="button" aria-label="Bookmark">', | ' <button class="gra-fab-btn" id="gra-fab-bookmark" type="button" aria-label="Bookmark">', | ||
' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>', | ' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>', | ||
' <span class="gra-fab-tooltip">Bookmark</span>', | ' <span class="gra-fab-tooltip">Bookmark</span>', | ||
' </button>', | |||
' <button class="gra-fab-btn" id="gra-fab-search" type="button" aria-label="Search this text">', | |||
' <span class="gra-icon gra-icon-search" aria-hidden="true"></span>', | |||
' <span class="gra-fab-tooltip">Search</span>', | |||
' </button>', | ' </button>', | ||
'</div>', | '</div>', | ||
| Line 101: | Line 93: | ||
$('body').append($fab); | $('body').append($fab); | ||
$mobileBar = $( [ | $mobileBar = $( [ | ||
'<div id="gra-mobile-bar" role="toolbar" aria-label="Actions">', | '<div id="gra-mobile-bar" role="toolbar" aria-label="Actions">', | ||
| Line 113: | Line 102: | ||
' <button class="gra-mob-btn" id="gra-mob-note" type="button">', | ' <button class="gra-mob-btn" id="gra-mob-note" type="button">', | ||
' <span class="gra-icon gra-icon-note" aria-hidden="true"></span>', | ' <span class="gra-icon gra-icon-note" aria-hidden="true"></span>', | ||
' <span class="gra-mob-label"> | ' <span class="gra-mob-label">Notes</span>', | ||
' </button>', | ' </button>', | ||
' <button class="gra-mob-btn" id="gra-mob-bookmark" type="button">', | ' <button class="gra-mob-btn" id="gra-mob-bookmark" type="button">', | ||
' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>', | ' <span class="gra-icon gra-icon-bookmark" aria-hidden="true"></span>', | ||
' <span class="gra-mob-label">Bookmark</span>', | ' <span class="gra-mob-label">Bookmark</span>', | ||
' </button>', | |||
' <button class="gra-mob-btn" id="gra-mob-search" type="button">', | |||
' <span class="gra-icon gra-icon-search" aria-hidden="true"></span>', | |||
' <span class="gra-mob-label">Search</span>', | |||
' </button>', | ' </button>', | ||
' <button class="gra-mob-btn gra-mob-dismiss" id="gra-mob-dismiss" type="button">', | ' <button class="gra-mob-btn gra-mob-dismiss" id="gra-mob-dismiss" type="button">', | ||
| Line 128: | Line 121: | ||
$('body').append($mobileBar); | $('body').append($mobileBar); | ||
$fbComposer = $( [ | $fbComposer = $( [ | ||
'<div class="gra-composer" id="gra-fb-composer" role="dialog" aria-label="Send feedback">', | '<div class="gra-composer" id="gra-fb-composer" role="dialog" aria-label="Send feedback">', | ||
| Line 159: | Line 151: | ||
$('body').append($fbComposer); | $('body').append($fbComposer); | ||
$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-composer-user">', | ' <div class="gra-composer-user">', | ||
' <div class="gra-avatar">' + esc(currentUser ? userInitial : '✎') + '</div>', | ' <div class="gra-avatar">' + esc(currentUser ? userInitial : '✎') + '</div>', | ||
' <div class="gra-composer-uname">' + esc(currentUser || ' | ' <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 175: | Line 166: | ||
$('body').append($ntComposer); | $('body').append($ntComposer); | ||
$bmComposer = $( [ | $bmComposer = $( [ | ||
'<div class="gra-bm-composer" id="gra-bm-composer" role="dialog" aria-label="Bookmark">', | '<div class="gra-bm-composer" id="gra-bm-composer" role="dialog" aria-label="Bookmark">', | ||
| Line 191: | Line 181: | ||
$('body').append($bmComposer); | $('body').append($bmComposer); | ||
$panel = $( [ | $panel = $( [ | ||
'<div id="gra-panel" role="complementary" aria-label="Notes">', | '<div id="gra-panel" role="complementary" aria-label="Notes">', | ||
| Line 227: | Line 216: | ||
$panel.hasClass('gra-panel-open') ? closePanel() : openPanel(_activeTab); | $panel.hasClass('gra-panel-open') ? closePanel() : openPanel(_activeTab); | ||
}); | }); | ||
$('#gra-panel-title').text(pageTitle.replace(/_/g,' ').split('/')[0].slice(0,30)); | $('#gra-panel-title').text(pageTitle.replace(/_/g,' ').split('/')[0].slice(0,30)); | ||
| Line 245: | Line 235: | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// SELECTION | // SELECTION — snapshot stored at selectionchange time | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
/** | |||
* captureSelection() | |||
* Called from the debounced selectionchange handler (not from button tap). | |||
* Stores a cloned Range in _selRange so it survives the selection being | |||
* cleared when the user taps the action bar button. | |||
*/ | |||
function captureSelection() { | function captureSelection() { | ||
var sel = window.getSelection(); | var sel = window.getSelection(); | ||
| Line 256: | Line 252: | ||
var contentEl = document.querySelector(CONTENT_SEL); | var contentEl = document.querySelector(CONTENT_SEL); | ||
if (!contentEl) return false; | if (!contentEl) return false; | ||
var | var ancestor = range.commonAncestorContainer; | ||
if ( | if (ancestor.nodeType === 3) ancestor = ancestor.parentNode; | ||
if (!contentEl.contains( | if (!ancestor || !contentEl.contains(ancestor)) return false; | ||
/* ── Editor mode guard (v5-patch1) ──────────────────────────── | |||
* Do not fire the annotation gadget when the selection is inside | |||
* the custom editor surface (#se-surface / .se-outer). | |||
* This is a purely additive check — no existing logic is changed. | |||
* ──────────────────────────────────────────────────────────── */ | |||
var _editorEl = document.getElementById('se-surface') || | |||
document.querySelector('.se-outer'); | |||
if ( _editorEl && _editorEl.contains(ancestor) ) return false; | |||
_selText = text; | _selText = text; | ||
_selRect = range.getBoundingClientRect(); | _selRect = range.getBoundingClientRect(); | ||
/* Clone the range NOW while selection is live */ | |||
try { _selRange = range.cloneRange(); } | |||
catch(e){ _selRange = null; } | |||
return true; | |||
} | |||
/** | |||
* reCaptureFromDOM() | |||
* Fallback when _selRange is null at button-tap time (selection already | |||
* collapsed). Tries to find the text in the DOM using _selText. | |||
*/ | |||
function reCaptureFromDOM() { | |||
if (!_selText) return false; | |||
var contentEl = document.querySelector(CONTENT_SEL); | |||
if (!contentEl) return false; | |||
var found = findTextInContent(contentEl, _selText.slice(0,80).replace(/…$/,'').trim()); | |||
if (!found) return false; | |||
_selRange = found; | |||
return true; | return true; | ||
} | } | ||
function tryShowActions() { | function tryShowActions() { | ||
if ($fbComposer.hasClass('gra-composer-visible')) return; | if ($fbComposer && $fbComposer.hasClass('gra-composer-visible')) return; | ||
if ($ntComposer.hasClass('gra-composer-visible')) return; | if ($ntComposer && $ntComposer.hasClass('gra-composer-visible')) return; | ||
if ($bmComposer.hasClass('gra-composer-visible')) return; | if ($bmComposer && $bmComposer.hasClass('gra-composer-visible')) return; | ||
if (!captureSelection()) { | if (!captureSelection()) { | ||
hideActions(); | hideActions(); | ||
| Line 286: | Line 311: | ||
function showFab(rect) { | function showFab(rect) { | ||
if (_mobile) return; | if (_mobile) return; | ||
if (!rect) return; | if (!rect) return; | ||
var fabW = 46, fabH = 126; | var fabW = 46, fabH = 126; | ||
| Line 303: | Line 328: | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
function showMobileBar() { | function showMobileBar() { $mobileBar.addClass('gra-mobile-bar-visible'); } | ||
function hideMobileBar() { $mobileBar.removeClass('gra-mobile-bar-visible'); } | |||
} | function hideActions() { hideFab(); hideMobileBar(); } | ||
// ════════════════════════════════════════════════════════════════════ | |||
// WRAP SELECTION — null-safe version | |||
// ════════════════════════════════════════════════════════════════════ | |||
function | function wrapSelection(id, cssClass) { | ||
var range = _selRange; | |||
_selRange = null; | |||
if (!range) return null; | |||
/* Verify the range endpoints are still in the document */ | |||
try { | |||
if (!document.contains(range.startContainer) || | |||
!document.contains(range.endContainer)) { | |||
return null; | |||
} | |||
} catch(e) { return null; } | |||
function makeSpan() { | |||
var sp = document.createElement('span'); | |||
sp.className = cssClass; | |||
sp.setAttribute('data-gra-id', id); | |||
return; | return sp; | ||
} | } | ||
/* Try surroundContents first (fails if range crosses element boundaries) */ | |||
try { | |||
var span = makeSpan(); | |||
range.surroundContents(span); | |||
/* Verify the span was actually inserted */ | |||
if (span.parentNode) return span; | |||
} catch(e) { /* fall through */ } | |||
/* Fallback: extractContents + re-insert */ | |||
try { | try { | ||
var frag = range.extractContents(); | |||
var sp2 = makeSpan(); | |||
sp2.appendChild(frag); | |||
range.insertNode(sp2); | |||
/* null-guard: check parentNode before returning */ | |||
if (sp2 && sp2.parentNode) return sp2; | |||
} catch(e2) { /* give up */ } | |||
return null; | |||
} | } | ||
| Line 434: | Line 441: | ||
setTimeout(closeFeedbackComposer, 2500); | setTimeout(closeFeedbackComposer, 2500); | ||
} | } | ||
function showFeedbackError(msg) { | function showFeedbackError(msg) { | ||
$fbSubmit.prop('disabled', false).text('Send'); | $fbSubmit.prop('disabled', false).text('Send'); | ||
| Line 446: | Line 452: | ||
function openNoteComposer() { | function openNoteComposer() { | ||
hideActions(); | hideActions(); | ||
$ntComposer.css({ top: '', left: '', transform: '' }); | $ntComposer.css({ top: '', left: '', transform: '' }); | ||
$ntComposer.addClass('gra-composer-visible'); | $ntComposer.addClass('gra-composer-visible'); | ||
| Line 467: | Line 472: | ||
var ts = nowIso(); | var ts = nowIso(); | ||
var quote = _selText.slice(0,120) + (_selText.length > 120 ? '…' : ''); | var quote = _selText.slice(0,120) + (_selText.length > 120 ? '…' : ''); | ||
/* _selRange may be null if selection was cleared — try re-capture */ | |||
if (!_selRange && _selText) reCaptureFromDOM(); | |||
var span = wrapSelection(id, 'gra-note-highlight'); | var span = wrapSelection(id, 'gra-note-highlight'); | ||
if (span) span.setAttribute('data-gra-quote', quote); | if (span) span.setAttribute('data-gra-quote', quote); | ||
| Line 490: | Line 497: | ||
function openBookmarkComposer() { | function openBookmarkComposer() { | ||
hideActions(); | hideActions(); | ||
$bmComposer.css({ top: '', left: '', transform: '' }); | $bmComposer.css({ top: '', left: '', transform: '' }); | ||
$bmComposer.addClass('gra-composer-visible'); | $bmComposer.addClass('gra-composer-visible'); | ||
| Line 508: | Line 514: | ||
var id = uid(); | var id = uid(); | ||
var quote = _selText.slice(0,120) + (_selText.length > 120 ? '…' : ''); | var quote = _selText.slice(0,120) + (_selText.length > 120 ? '…' : ''); | ||
if (!_selRange && _selText) reCaptureFromDOM(); | |||
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); } | ||
| Line 520: | Line 527: | ||
_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 && span.parentNode) { | ||
var p = span.parentNode; | var p = span.parentNode; | ||
while (span.firstChild) p.insertBefore(span.firstChild, span); | while (span.firstChild) p.insertBefore(span.firstChild, span); | ||
| Line 588: | Line 595: | ||
_notes = _notes.filter(function(n){ return n.id !== id; }); | _notes = _notes.filter(function(n){ return n.id !== id; }); | ||
var span = document.querySelector('[data-gra-id="'+id+'"].gra-note-highlight'); | var span = document.querySelector('[data-gra-id="'+id+'"].gra-note-highlight'); | ||
if (span) { | if (span && span.parentNode) { | ||
var p = span.parentNode; | var p = span.parentNode; | ||
while (span.firstChild) p.insertBefore(span.firstChild, span); | while (span.firstChild) p.insertBefore(span.firstChild, span); | ||
| Line 638: | Line 645: | ||
function wireEvents() { | function wireEvents() { | ||
/ | /* ── Desktop mouseup ── */ | ||
$(document).on('mouseup', function(e){ | $(document).on('mouseup', function(e){ | ||
if (e.button !== 0) return; | if (e.button !== 0) return; | ||
if (_mobile) return; | if (_mobile) return; | ||
setTimeout(tryShowActions, 20); | setTimeout(tryShowActions, 20); | ||
}); | }); | ||
/ | /* ── Mobile: touchend triggers selection check ── | ||
* On Minerva, selectionchange fires during drag but the range | |||
// | * isn't stable until touchend. We listen for touchend too and | ||
* check after a short delay for the selection to settle. */ | |||
document.addEventListener('touchend', function() { | |||
if (!_mobile) return; | |||
clearTimeout(_selTimer); | |||
_selTimer = setTimeout(tryShowActions, 400); | |||
}, { passive: true }); | |||
/* ── selectionchange debounced 600ms ── */ | |||
var _selTimer = null; | var _selTimer = null; | ||
document.addEventListener('selectionchange', function() { | document.addEventListener('selectionchange', function() { | ||
| Line 657: | Line 672: | ||
if (_fabSelVer === v) return; | if (_fabSelVer === v) return; | ||
tryShowActions(); | tryShowActions(); | ||
}, 600); | }, 600); | ||
}); | }); | ||
/ | /* ── Click outside → hide actions ── */ | ||
$(document).on('mousedown touchstart', function(e){ | $(document).on('mousedown touchstart', function(e){ | ||
var t = e.target; | var t = e.target; | ||
| Line 671: | Line 686: | ||
}); | }); | ||
/ | /* ── Desktop FAB ── */ | ||
$('#gra-fab-feedback').on('click', function(e){ | $('#gra-fab-feedback').on('click', function(e){ | ||
e.preventDefault(); e.stopPropagation(); | e.preventDefault(); e.stopPropagation(); | ||
if (!_selRange | if (!_selRange) captureSelection(); | ||
if (!_selRange) return; | |||
openFeedbackComposer(); | openFeedbackComposer(); | ||
}); | }); | ||
$('#gra-fab-note').on('click', function(e){ | $('#gra-fab-note').on('click', function(e){ | ||
e.preventDefault(); e.stopPropagation(); | e.preventDefault(); e.stopPropagation(); | ||
if (!_selRange | if (!_selRange) captureSelection(); | ||
if (!_selRange) return; | |||
openNoteComposer(); | openNoteComposer(); | ||
}); | }); | ||
$('#gra-fab-bookmark').on('click', function(e){ | $('#gra-fab-bookmark').on('click', function(e){ | ||
e.preventDefault(); e.stopPropagation(); | e.preventDefault(); e.stopPropagation(); | ||
if (!_selRange | if (!_selRange) captureSelection(); | ||
if (!_selRange) return; | |||
openBookmarkComposer(); | openBookmarkComposer(); | ||
}); | |||
$('#gra-fab-search').on('click', function(e){ | |||
e.preventDefault(); e.stopPropagation(); | |||
var q = _selText; | |||
hideActions(); | |||
_selRange = null; _selText = ''; _selRect = null; | |||
if (q && window.showSearchDialog) { window.showSearchDialog(q); } | |||
else if (q) { /* trigger readerToolbar search shortcut */ $(document).trigger($.Event('keydown', {ctrlKey:true, key:'k', keyCode:75})); } | |||
}); | }); | ||
/ | /* ── Mobile bottom bar ── | ||
* _selRange was already stored when selectionchange fired 700ms | |||
* earlier. We use it directly — no need to call captureSelection() | |||
* again (selection is likely already collapsed from the tap). */ | |||
$('#gra-mob-feedback').on('click touchend', function(e){ | $('#gra-mob-feedback').on('click touchend', function(e){ | ||
e.preventDefault(); e.stopPropagation(); | e.preventDefault(); e.stopPropagation(); | ||
hideMobileBar(); | hideMobileBar(); | ||
if (!_selRange && ! | if (!_selRange && !reCaptureFromDOM()) return; | ||
openFeedbackComposer(); | openFeedbackComposer(); | ||
}); | }); | ||
| Line 698: | Line 727: | ||
e.preventDefault(); e.stopPropagation(); | e.preventDefault(); e.stopPropagation(); | ||
hideMobileBar(); | hideMobileBar(); | ||
if (!_selRange && ! | if (!_selRange && !reCaptureFromDOM()) return; | ||
openNoteComposer(); | openNoteComposer(); | ||
}); | }); | ||
| Line 704: | Line 733: | ||
e.preventDefault(); e.stopPropagation(); | e.preventDefault(); e.stopPropagation(); | ||
hideMobileBar(); | hideMobileBar(); | ||
if (!_selRange && ! | if (!_selRange && !reCaptureFromDOM()) return; | ||
openBookmarkComposer(); | openBookmarkComposer(); | ||
}); | |||
$('#gra-mob-search').on('click touchend', function(e){ | |||
e.preventDefault(); e.stopPropagation(); | |||
var q = _selText; | |||
hideMobileBar(); | |||
_selRange = null; _selText = ''; _selRect = null; | |||
if (window.getSelection) window.getSelection().removeAllRanges(); | |||
if (q && window.showSearchDialog) { setTimeout(function(){ window.showSearchDialog(q); }, 50); } | |||
}); | }); | ||
$('#gra-mob-dismiss').on('click touchend', function(e){ | $('#gra-mob-dismiss').on('click touchend', function(e){ | ||
e.preventDefault(); e.stopPropagation(); | e.preventDefault(); e.stopPropagation(); | ||
hideMobileBar(); | hideMobileBar(); | ||
_selRange = null; _selText = ''; _selRect = null; | |||
if (window.getSelection) window.getSelection().removeAllRanges(); | if (window.getSelection) window.getSelection().removeAllRanges(); | ||
}); | }); | ||
/ | /* ── Feedback composer ── */ | ||
$fbIssueType.on('change', function(){ | $fbIssueType.on('change', function(){ $fbSubmit.prop('disabled', !$(this).val()); }); | ||
$('#gra-fb-cancel, #gra-fb-close').on('click', closeFeedbackComposer); | |||
$('#gra-fb-cancel, #gra-fb-close').on('click', | |||
$fbSubmit.on('click', submitFeedback); | $fbSubmit.on('click', submitFeedback); | ||
$fbText.on('keydown', function(e){ | $fbText.on('keydown', function(e){ if(e.key==='Escape') closeFeedbackComposer(); }); | ||
/ | /* ── Note composer ── */ | ||
$ntInput.on('input', function(){ | $ntInput.on('input', function(){ $ntSubmit.prop('disabled', !$(this).val().trim()); }); | ||
$('#gra-nt-cancel').on('click', closeNoteComposer); | $('#gra-nt-cancel').on('click', closeNoteComposer); | ||
$ntSubmit.on('click', submitNote); | $ntSubmit.on('click', submitNote); | ||
| Line 737: | Line 766: | ||
}); | }); | ||
/ | /* ── Bookmark composer ── */ | ||
$('#gra-bm-cancel').on('click', closeBookmarkComposer); | $('#gra-bm-cancel').on('click', closeBookmarkComposer); | ||
$bmSubmit.on('click', submitBookmark); | $bmSubmit.on('click', submitBookmark); | ||
| Line 745: | Line 774: | ||
}); | }); | ||
/ | /* ── Panel ── */ | ||
$('#gra-panel-close').on('click', closePanel); | $('#gra-panel-close').on('click', closePanel); | ||
$backdrop.on('click touchend', function(e){ | $backdrop.on('click touchend', function(e){ | ||
| Line 830: | Line 859: | ||
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) return; | ||
var sp = document.createElement('span'); | |||
sp.className = 'gra-note-highlight'; | |||
sp.setAttribute('data-gra-id', h.id); | |||
try { range.surroundContents(sp); } catch(e){ /* skip if crossing boundaries */ } | |||
}); | }); | ||
} | } | ||
| Line 846: | Line 874: | ||
if (!needle) return; | if (!needle) return; | ||
var found = findTextInContent(document.querySelector(CONTENT_SEL), needle); | var found = findTextInContent(document.querySelector(CONTENT_SEL), needle); | ||
if (found) | if (!found) return; | ||
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 { found.surroundContents(sp); } catch(e){} | |||
}); | }); | ||
} | } | ||
function findTextInContent(root, needle) { | function findTextInContent(root, needle) { | ||
if (!root) return null; | if (!root || !needle) return null; | ||
var text = root.textContent || ''; | var text = root.textContent || ''; | ||
var idx = text.indexOf(needle); | var idx = text.indexOf(needle); | ||
| Line 871: | Line 898: | ||
} | } | ||
if (!startNode || !endNode) return null; | if (!startNode || !endNode) return null; | ||
var r = document.createRange(); | try { | ||
var r = document.createRange(); | |||
r.setStart(startNode, startOffset); | |||
return | r.setEnd(endNode, endOffset); | ||
return r; | |||
} catch(e){ return null; } | |||
} | } | ||
| Line 882: | Line 911: | ||
$(function() { | $(function() { | ||
_mobile = window.innerWidth < 768 || 'ontouchstart' in window; | _mobile = window.innerWidth < 768 || 'ontouchstart' in window; | ||
window.addEventListener('resize', function(){ | window.addEventListener('resize', function(){ | ||
| Line 892: | Line 920: | ||
loadNotes(); | loadNotes(); | ||
loadBookmarks(); | loadBookmarks(); | ||
restoreNoteHighlights(); | |||
/* Restore highlights in a setTimeout so they don't block page paint */ | |||
setTimeout(function(){ | |||
try { restoreNoteHighlights(); } catch(e){} | |||
try { restoreBookmarkHighlights(); } catch(e){} | |||
}, 500); | |||
}); | }); | ||
}() ); | }() ); | ||