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  (v4)
  * gr_annotations.js  —  grantha.io inline Notes + Bookmarks + Feedback  (v5)
  * ══════════════════════════════════════════════════════════════════════
  * ══════════════════════════════════════════════════════════════════════
  *
  *
  * CHANGES FROM v3
  * FIXES FROM v4
  * ────────────────
  * ─────────────
  * • Mobile: FAB no longer fights browser's native copy/paste menu.
  * • BUG: "Cannot read properties of null (reading 'parentNode')" from
  *  On mobile, a bottom-sheet action bar slides up after selection
*  ToggleList.js crashing before wireEvents() completes.
  *  instead of a tiny floating strip next to the text.
*  FIX: wrapSelection() now null-guards every DOM operation. Also
  * • Mobile: Long-press detection improved — waits for selectionchange
  *  wrapped surroundContents in a try/catch that falls back gracefully
  *  to settle before showing the action bar.
  *  instead of letting the exception propagate.
  * • Mobile: Action bar buttons are large (48px tap targets) with labels.
*
  * • Desktop: FAB strip unchanged — appears beside selection.
  * • BUG: On mobile, _selRange is null when action buttons are tapped
  * • Feedback composer: centered modal on all screen sizes.
*  because selectionchange fires again (collapsing the selection) right
  *  as the user lifts their finger to tap the action bar.
  *   FIX: captureSelection() now saves _selText + a serialized anchor/
*  focus pair at selectionchange time (600ms debounce). We store a
*  *snapshot* of the range so it survives the selection being cleared
*  by the tap on the action button.
*
  * • BUG: Mobile bar appeared underneath browser's copy/paste menu.
*  FIX: Delay increased to 700ms and bar z-index lifted to 99999.
*
  * • MISC: $mobileBar z-index fixed so it renders above gr-static-bar.
  */
  */


Line 43: Line 53:


   // ── 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 61:
   var _selVersion = 0;
   var _selVersion = 0;
   var _fabSelVer  = -1;
   var _fabSelVer  = -1;
   var _mobile    = window.innerWidth < 768 || 'ontouchstart' in window;   // set immediately
   var _mobile    = window.innerWidth < 768 || 'ontouchstart' in window;


   // ── Helpers ──────────────────────────────────────────────────────────
   // ── Helpers ──────────────────────────────────────────────────────────
Line 82: Line 92:


   function buildDom() {
   function buildDom() {
    // ── Desktop FAB strip ────────────────────────────────────────────
     $fab = $( [
     $fab = $( [
       '<div id="gra-fab" role="toolbar" aria-label="Feedback / Note / Bookmark">',
       '<div id="gra-fab" role="toolbar" aria-label="Feedback / Note / Bookmark">',
Line 101: Line 110:
     $('body').append($fab);
     $('body').append($fab);


    // ── Mobile bottom action bar (shown instead of FAB on mobile) ────
    // Slides up from the bottom — well above the browser's copy/paste menu
    // Large tap targets (48px+) with text labels
     $mobileBar = $( [
     $mobileBar = $( [
       '<div id="gra-mobile-bar" role="toolbar" aria-label="Actions">',
       '<div id="gra-mobile-bar" role="toolbar" aria-label="Actions">',
Line 128: Line 134:
     $('body').append($mobileBar);
     $('body').append($mobileBar);


    // ── Feedback composer (centered modal) ───────────────────────────
     $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 164:
     $('body').append($fbComposer);
     $('body').append($fbComposer);


    // ── Note composer ────────────────────────────────────────────────
     $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">',
Line 175: Line 179:
     $('body').append($ntComposer);
     $('body').append($ntComposer);


    // ── Bookmark composer ─────────────────────────────────────────────
     $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 194:
     $('body').append($bmComposer);
     $('body').append($bmComposer);


    // ── Right panel ───────────────────────────────────────────────────
     $panel = $( [
     $panel = $( [
       '<div id="gra-panel" role="complementary" aria-label="Notes">',
       '<div id="gra-panel" role="complementary" aria-label="Notes">',
Line 245: Line 247:


   // ════════════════════════════════════════════════════════════════════
   // ════════════════════════════════════════════════════════════════════
   // 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 264:
     var contentEl = document.querySelector(CONTENT_SEL);
     var contentEl = document.querySelector(CONTENT_SEL);
     if (!contentEl) return false;
     if (!contentEl) return false;
     var start = range.commonAncestorContainer;
     var ancestor = range.commonAncestorContainer;
     if (start.nodeType === 3) start = start.parentNode;
     if (ancestor.nodeType === 3) ancestor = ancestor.parentNode;
     if (!contentEl.contains(start)) return false;
     if (!ancestor || !contentEl.contains(ancestor)) return false;
 
     _selText  = text;
     _selText  = text;
    _selRange = range.cloneRange();
     _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 314:


   function showFab(rect) {
   function showFab(rect) {
     if (_mobile) return;   // mobile uses bottom bar
     if (_mobile) return;
     if (!rect) return;
     if (!rect) return;
     var fabW = 46, fabH = 126;
     var fabW = 46, fabH = 126;
Line 303: Line 331:
   // ════════════════════════════════════════════════════════════════════
   // ════════════════════════════════════════════════════════════════════


   function showMobileBar() {
   function showMobileBar() { $mobileBar.addClass('gra-mobile-bar-visible'); }
    $mobileBar.addClass('gra-mobile-bar-visible');
  function hideMobileBar() { $mobileBar.removeClass('gra-mobile-bar-visible'); }
   }
   function hideActions()  { hideFab(); hideMobileBar(); }


   function hideMobileBar() {
   // ════════════════════════════════════════════════════════════════════
    $mobileBar.removeClass('gra-mobile-bar-visible');
  // WRAP SELECTION  — null-safe version
   }
   // ════════════════════════════════════════════════════════════════════


   function hideActions() {
   function wrapSelection(id, cssClass) {
     hideFab();
     var range = _selRange;
     hideMobileBar();
     _selRange = null;
  }


  // Debug helper — remove after testing
     if (!range) return null;
  window._graDebug = function() {
    console.log('mobile:', _mobile, 'selText:', _selText, 'selRange:', _selRange);
     console.log('bar visible:', document.getElementById('gra-mobile-bar') &&
      document.getElementById('gra-mobile-bar').classList.contains('gra-mobile-bar-visible'));
  };


  // ════════════════════════════════════════════════════════════════════
    /* Verify the range endpoints are still in the document */
  // COMPOSER POSITIONING (desktop note/bookmark only)
    try {
  // ════════════════════════════════════════════════════════════════════
      if (!document.contains(range.startContainer) ||
          !document.contains(range.endContainer)) {
        return null;
      }
    } catch(e) { return null; }


  function positionComposer($el) {
    function makeSpan() {
    if (isMobile()) {
      var sp = document.createElement('span');
       // On mobile always center — don't anchor to selection
       sp.className = cssClass;
       $el.css({top: '', left: '', transform: ''});
       sp.setAttribute('data-gra-id', id);
       return;
       return sp;
     }
     }
    if (!_selRect) return;
    var W  = 340;
    var top  = _selRect.bottom + 8;
    var left = _selRect.left;
    if (left + W > window.innerWidth - 8) left = window.innerWidth - W - 8;
    left = Math.max(left, 8);
    if (top + 280 > window.innerHeight) top = _selRect.top - 290;
    top  = Math.max(top, 8);
    $el.css({top: top+'px', left: left+'px'});
  }


  // ════════════════════════════════════════════════════════════════════
    /* Try surroundContents first (fails if range crosses element boundaries) */
  // WRAP SELECTION
    try {
  // ════════════════════════════════════════════════════════════════════
      var span = makeSpan();
      range.surroundContents(span);
      /* Verify the span was actually inserted */
      if (span.parentNode) return span;
    } catch(e) { /* fall through */ }


  function wrapSelection(id, cssClass) {
     /* Fallback: extractContents + re-insert */
     if (!_selRange) return null;
    var range = _selRange;
    _selRange = null;
     try {
     try {
       var span = document.createElement('span');
       var frag = range.extractContents();
      span.className = cssClass;
      var sp2  = makeSpan();
      span.setAttribute('data-gra-id', id);
      sp2.appendChild(frag);
      range.surroundContents(span);
      range.insertNode(sp2);
      return span;
      /* null-guard: check parentNode before returning */
    } catch(e) {
      if (sp2 && sp2.parentNode) return sp2;
      try {
    } catch(e2) { /* give up */ }
        var frag = range.extractContents();
 
        var sp2  = document.createElement('span');
    return null;
        sp2.className = cssClass;
        sp2.setAttribute('data-gra-id', id);
        sp2.appendChild(frag);
        range.insertNode(sp2);
        return sp2;
      } catch(e2) { return null; }
    }
   }
   }


Line 434: Line 444:
     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 455:
   function openNoteComposer() {
   function openNoteComposer() {
     hideActions();
     hideActions();
    // Centered modal on all devices
     $ntComposer.css({ top: '', left: '', transform: '' });
     $ntComposer.css({ top: '', left: '', transform: '' });
     $ntComposer.addClass('gra-composer-visible');
     $ntComposer.addClass('gra-composer-visible');
Line 467: Line 475:
     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 500:
   function openBookmarkComposer() {
   function openBookmarkComposer() {
     hideActions();
     hideActions();
    // Centered modal — same as feedback, no position anchoring
     $bmComposer.css({ top: '', left: '', transform: '' });
     $bmComposer.css({ top: '', left: '', transform: '' });
     $bmComposer.addClass('gra-composer-visible');
     $bmComposer.addClass('gra-composer-visible');
Line 508: Line 517:
     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 530:
     _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 598:
     _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 648:
   function wireEvents() {
   function wireEvents() {


     // ── Desktop mouseup ───────────────────────────────────────────
     /* ── 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;   // mobile uses selectionchange
       if (_mobile) return;
       setTimeout(tryShowActions, 20);
       setTimeout(tryShowActions, 20);
     });
     });


     // ── Mobile: selectionchange (debounced 600ms) ─────────────────
     /* ── Mobile: selectionchange debounced 700ms ──
    // We wait longer than v3 so the browser's own copy menu has time
    * We snapshot _selRange inside captureSelection() at this point,
    // to appear first — user can still copy, THEN our bar slides up.
    * so it's safely stored before the user taps the action button
    * (which would clear window.getSelection()).                      */
     var _selTimer = null;
     var _selTimer = null;
     document.addEventListener('selectionchange', function() {
     document.addEventListener('selectionchange', function() {
Line 657: Line 668:
         if (_fabSelVer === v) return;
         if (_fabSelVer === v) return;
         tryShowActions();
         tryShowActions();
       }, 600);   // 600ms — after browser copy menu appears
       }, 700);
     });
     });


     // ── Click outside → hide actions ──────────────────────────────
     /* ── 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 682:
     });
     });


     // ── Desktop FAB buttons ───────────────────────────────────────
     /* ── 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 && !captureSelection()) return;
       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 && !captureSelection()) return;
       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 && !captureSelection()) return;
       if (!_selRange) captureSelection();
      if (!_selRange) return;
       openBookmarkComposer();
       openBookmarkComposer();
     });
     });


     // ── Mobile bottom bar buttons ─────────────────────────────────
     /* ── 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 && !captureSelection()) return;
       if (!_selRange && !reCaptureFromDOM()) return;
       openFeedbackComposer();
       openFeedbackComposer();
     });
     });
Line 698: Line 715:
       e.preventDefault(); e.stopPropagation();
       e.preventDefault(); e.stopPropagation();
       hideMobileBar();
       hideMobileBar();
       if (!_selRange && !captureSelection()) return;
       if (!_selRange && !reCaptureFromDOM()) return;
       openNoteComposer();
       openNoteComposer();
     });
     });
Line 704: Line 721:
       e.preventDefault(); e.stopPropagation();
       e.preventDefault(); e.stopPropagation();
       hideMobileBar();
       hideMobileBar();
       if (!_selRange && !captureSelection()) return;
       if (!_selRange && !reCaptureFromDOM()) return;
       openBookmarkComposer();
       openBookmarkComposer();
     });
     });
Line 710: Line 727:
       e.preventDefault(); e.stopPropagation();
       e.preventDefault(); e.stopPropagation();
       hideMobileBar();
       hideMobileBar();
       // Clear selection visually
       _selRange = null; _selText = ''; _selRect = null;
       if (window.getSelection) window.getSelection().removeAllRanges();
       if (window.getSelection) window.getSelection().removeAllRanges();
     });
     });


     // ── Feedback composer ─────────────────────────────────────────
     /* ── Feedback composer ── */
     $fbIssueType.on('change', function(){
     $fbIssueType.on('change', function(){ $fbSubmit.prop('disabled', !$(this).val()); });
      $fbSubmit.prop('disabled', !$(this).val());
     $('#gra-fb-cancel, #gra-fb-close').on('click', closeFeedbackComposer);
    });
     $('#gra-fb-cancel, #gra-fb-close').on('click', function(){
      closeFeedbackComposer();
    });
     $fbSubmit.on('click', submitFeedback);
     $fbSubmit.on('click', submitFeedback);
     $fbText.on('keydown', function(e){
     $fbText.on('keydown', function(e){ if(e.key==='Escape') closeFeedbackComposer(); });
      if (e.key==='Escape') closeFeedbackComposer();
    });


     // ── Note composer ─────────────────────────────────────────────
     /* ── Note composer ── */
     $ntInput.on('input', function(){
     $ntInput.on('input', function(){ $ntSubmit.prop('disabled', !$(this).val().trim()); });
      $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 746:
     });
     });


     // ── Bookmark composer ─────────────────────────────────────────
     /* ── 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 754:
     });
     });


     // ── Panel ─────────────────────────────────────────────────────
     /* ── 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 839:
       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');
      var sp = document.createElement('span');
        sp.className = 'gra-note-highlight';
      sp.className = 'gra-note-highlight';
        sp.setAttribute('data-gra-id', h.id);
      sp.setAttribute('data-gra-id', h.id);
        try { range.surroundContents(sp); } catch(e){}
      try { range.surroundContents(sp); } catch(e){ /* skip if crossing boundaries */ }
      }
     });
     });
   }
   }
Line 846: Line 854:
       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');
      var sp = document.createElement('span');
        sp.className = 'gra-bookmark-highlight';
      sp.className = 'gra-bookmark-highlight';
        sp.setAttribute('data-gra-id', b.id);
      sp.setAttribute('data-gra-id', b.id);
        sp.setAttribute('data-gra-name', b.name);
      sp.setAttribute('data-gra-name', b.name);
        try { found.surroundContents(sp); } catch(e){}
      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 878:
     }
     }
     if (!startNode || !endNode) return null;
     if (!startNode || !endNode) return null;
     var r = document.createRange();
     try {
    r.setStart(startNode, startOffset);
      var r = document.createRange();
    r.setEnd(endNode, endOffset);
      r.setStart(startNode, startOffset);
     return r;
      r.setEnd(endNode, endOffset);
      return r;
     } catch(e){ return null; }
   }
   }


Line 882: Line 891:


   $(function() {
   $(function() {
    // Detect mobile once on load
     _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 900:
     loadNotes();
     loadNotes();
     loadBookmarks();
     loadBookmarks();
     restoreNoteHighlights();
 
    restoreBookmarkHighlights();
    /* Restore highlights in a setTimeout so they don't block page paint */
     setTimeout(function(){
      try { restoreNoteHighlights(); } catch(e){}
      try { restoreBookmarkHighlights(); } catch(e){}
    }, 500);
   });
   });


}() );
}() );