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 (v4) | ||
* ══════════════════════════════════════════════════════════════════════ | * ══════════════════════════════════════════════════════════════════════ | ||
* | * | ||
* CHANGES FROM | * CHANGES FROM v3 | ||
* ──────────────── | * ──────────────── | ||
* • | * • Mobile: FAB no longer fights browser's native copy/paste menu. | ||
* | * On mobile, a bottom-sheet action bar slides up after selection | ||
* instead of a tiny floating strip next to the text. | |||
* • Mobile: Long-press detection improved — waits for selectionchange | |||
* | * to settle before showing the action bar. | ||
* • Mobile: Action bar buttons are large (48px tap targets) with labels. | |||
* | * • Desktop: FAB strip unchanged — appears beside selection. | ||
* • Feedback composer: centered modal on all screen sizes. | |||
* | |||
* | |||
* • | |||
* | |||
*/ | */ | ||
| Line 25: | Line 20: | ||
// ── Configuration ──────────────────────────────────────────────────── | // ── Configuration ──────────────────────────────────────────────────── | ||
var CONTENT_SEL = '#mw-content-text'; | var CONTENT_SEL = '#mw-content-text'; | ||
var BM_LS_KEY = 'grantha_bm_' + ( ( window.mw && mw.config.get( 'wgPageName' ) ) || '' ); | var BM_LS_KEY = 'grantha_bm_' + ( ( window.mw && mw.config.get( 'wgPageName' ) ) || '' ); | ||
| Line 32: | Line 26: | ||
var currentUser = ( window.mw && mw.config.get( 'wgUserName' ) ) || ''; | var currentUser = ( window.mw && mw.config.get( 'wgUserName' ) ) || ''; | ||
var userInitial = currentUser ? currentUser.charAt( 0 ).toUpperCase() : '?'; | var userInitial = currentUser ? currentUser.charAt( 0 ).toUpperCase() : '?'; | ||
var currentUserEmail = ''; | var currentUserEmail = ''; | ||
if ( currentUser && window.mw ) { | if ( currentUser && window.mw ) { | ||
new mw.Api().get({ | new mw.Api().get({ | ||
action: 'query', | action: 'query', meta: 'userinfo', uiprop: 'email', formatversion: 2, | ||
}).then( function (data) { | |||
}).then( function ( data ) { | |||
var info = data && data.query && data.query.userinfo; | var info = data && data.query && data.query.userinfo; | ||
if ( info && info.email ) currentUserEmail = info.email; | if ( info && info.email ) currentUserEmail = info.email; | ||
| Line 56: | Line 46: | ||
var _selText = ''; | var _selText = ''; | ||
var _selRect = null; | var _selRect = null; | ||
var _notes = []; | var _notes = []; | ||
var _bookmarks = []; | var _bookmarks = []; | ||
var _activeTab = 'notes'; | var _activeTab = 'notes'; | ||
var _selVersion = 0; | var _selVersion = 0; | ||
var _fabSelVer = -1; | var _fabSelVer = -1; | ||
var _mobile = false; // set on init | |||
// ── Helpers ────────────────────────────────────────────────────────── | // ── Helpers ────────────────────────────────────────────────────────── | ||
function uid() { | function uid() { return 'gra_' + Date.now() + '_' + Math.random().toString(36).slice(2,7); } | ||
function esc(s) { | |||
return String(s||'').replace(/&/g,'&').replace(/</g,'<') | |||
function esc( s ) { | .replace(/>/g,'>').replace(/"/g,'"'); | ||
return String( s || '' ) | |||
} | } | ||
function nowIso() { return new Date().toISOString().replace(/\.\d{3}Z$/,'Z'); } | function nowIso() { return new Date().toISOString().replace(/\.\d{3}Z$/,'Z'); } | ||
function fmtTs( ts ) { | function fmtTs(ts) { | ||
try { | try { | ||
var d = new Date( ts ); | var d = new Date(ts); | ||
return d.toLocaleDateString('en-IN',{day:'numeric',month:'short',year:'numeric'}) | return d.toLocaleDateString('en-IN',{day:'numeric',month:'short',year:'numeric'}) | ||
+ ' ' + d.toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit',hour12:false}); | + ' ' + d.toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit',hour12:false}); | ||
} catch(e){ return ts; } | } catch(e){ return ts; } | ||
} | } | ||
function clamp( v, lo, hi ) { return Math.max(lo, Math.min(hi, v)); } | function clamp(v,lo,hi){ return Math.max(lo,Math.min(hi,v)); } | ||
function isMobile() { return | function isMobile() { return _mobile; } | ||
// ── DOM references ─────────────────────────────────────────────────── | // ── DOM references ─────────────────────────────────────────────────── | ||
var $fab, $panel, $backdrop; | var $fab, $mobileBar, $panel, $backdrop; | ||
var $ntComposer, $ntInput, $ntSubmit; | var $ntComposer, $ntInput, $ntSubmit; | ||
var $bmComposer, $bmInput, $bmSubmit; | var $bmComposer, $bmInput, $bmSubmit; | ||
var $fbComposer, $fbIssueType, $fbText, $fbEmail, $fbSubmit, $fbQuote; | var $fbComposer, $fbIssueType, $fbText, $fbEmail, $fbSubmit, $fbQuote; | ||
var $tabNotes, $tabBookmarks | var $tabNotes, $tabBookmarks, $paneNotes, $paneBookmarks; | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
| Line 95: | Line 82: | ||
function buildDom() { | function buildDom() { | ||
// ── Desktop FAB strip (hidden on mobile) ───────────────────────── | |||
// ── 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">', | ||
' <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>', | ||
' <span class="gra-fab-tooltip">Feedback</span>', | ' <span class="gra-fab-tooltip">Feedback</span>', | ||
' </button>', | ' </button>', | ||
' <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">Note</span>', | ' <span class="gra-fab-tooltip">Note</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>', | ||
| Line 116: | Line 99: | ||
'</div>', | '</div>', | ||
].join('') ); | ].join('') ); | ||
$( '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 = $( [ | |||
'<div id="gra-mobile-bar" role="toolbar" aria-label="Actions">', | |||
' <div id="gra-mobile-bar-inner">', | |||
' <button class="gra-mob-btn" id="gra-mob-feedback" type="button">', | |||
' <span class="gra-icon gra-icon-feedback" aria-hidden="true"></span>', | |||
' <span class="gra-mob-label">Feedback</span>', | |||
' </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-mob-label">Note</span>', | |||
' </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-mob-label">Bookmark</span>', | |||
' </button>', | |||
' <button class="gra-mob-btn gra-mob-dismiss" id="gra-mob-dismiss" type="button">', | |||
' <span style="font-size:20px;line-height:1">✕</span>', | |||
' <span class="gra-mob-label">Dismiss</span>', | |||
' </button>', | |||
' </div>', | |||
'</div>', | |||
].join('') ); | |||
$('body').append($mobileBar); | |||
// ── Feedback composer ( | // ── 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 137: | Line 147: | ||
' </select>', | ' </select>', | ||
' <div class="gra-fb-field-label">Details (optional)</div>', | ' <div class="gra-fb-field-label">Details (optional)</div>', | ||
' <textarea class="gra-composer-input" id="gra-fb-text" | ' <textarea class="gra-composer-input" id="gra-fb-text" placeholder="Describe the issue…" rows="3"></textarea>', | ||
' <div class="gra-fb-field-label">Your email (optional)</div>', | ' <div class="gra-fb-field-label">Your email (optional)</div>', | ||
' <input class="gra-composer-input gra-fb-email-input" id="gra-fb-email" | ' <input class="gra-composer-input gra-fb-email-input" id="gra-fb-email" type="email" placeholder="you@example.com" autocomplete="email">', | ||
' <div class="gra-composer-actions">', | ' <div class="gra-composer-actions">', | ||
' <button class="gra-btn-cancel" id="gra-fb-cancel">Cancel</button>', | ' <button class="gra-btn-cancel" id="gra-fb-cancel">Cancel</button>', | ||
| Line 149: | Line 157: | ||
'</div>', | '</div>', | ||
].join('') ); | ].join('') ); | ||
$( 'body' ).append( $fbComposer ); | $('body').append($fbComposer); | ||
// ── Note composer | // ── 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">', | ||
' <div class="gra-composer-user">', | ' <div class="gra-composer-user">', | ||
' <div class="gra | ' <div class="gra-avatar">' + esc(currentUser ? userInitial : '✎') + '</div>', | ||
' <div class="gra-composer-uname">' + esc(currentUser || 'Note') + '</div>', | ' <div class="gra-composer-uname">' + esc(currentUser || 'Note') + '</div>', | ||
' </div>', | ' </div>', | ||
' <textarea class="gra-composer-input" id="gra-nt-input" | ' <textarea class="gra-composer-input" id="gra-nt-input" placeholder="Write a note…" rows="3"></textarea>', | ||
' <div class="gra-composer-actions">', | ' <div class="gra-composer-actions">', | ||
' <button class="gra-btn-cancel" id="gra-nt-cancel">Cancel</button>', | ' <button class="gra-btn-cancel" id="gra-nt-cancel">Cancel</button>', | ||
| Line 166: | Line 173: | ||
'</div>', | '</div>', | ||
].join('') ); | ].join('') ); | ||
$( 'body' ).append( $ntComposer ); | $('body').append($ntComposer); | ||
// ── Bookmark composer ───────────────────────────────────────────── | // ── Bookmark composer ───────────────────────────────────────────── | ||
| Line 175: | Line 182: | ||
' Save bookmark', | ' Save bookmark', | ||
' </div>', | ' </div>', | ||
' <input class="gra-composer-input" id="gra-bm-input" | ' <input class="gra-composer-input" id="gra-bm-input" type="text" placeholder="Name this bookmark…" autocomplete="off">', | ||
' <div class="gra-composer-actions">', | ' <div class="gra-composer-actions">', | ||
' <button class="gra-btn-cancel" id="gra-bm-cancel">Cancel</button>', | ' <button class="gra-btn-cancel" id="gra-bm-cancel">Cancel</button>', | ||
| Line 183: | Line 189: | ||
'</div>', | '</div>', | ||
].join('') ); | ].join('') ); | ||
$( 'body' ).append( $bmComposer ); | $('body').append($bmComposer); | ||
// ── Right panel ─────────────────────────────────────────────────── | // ── Right panel ─────────────────────────────────────────────────── | ||
| Line 202: | Line 208: | ||
' <div id="gra-panel-body">', | ' <div id="gra-panel-body">', | ||
' <div class="gra-pane gra-pane-active" id="gra-pane-notes"></div>', | ' <div class="gra-pane gra-pane-active" id="gra-pane-notes"></div>', | ||
' <div class="gra-pane" | ' <div class="gra-pane" id="gra-pane-bookmarks"></div>', | ||
' </div>', | ' </div>', | ||
'</div>', | '</div>', | ||
].join('') ); | ].join('') ); | ||
$( 'body' ).append( $panel ); | $('body').append($panel); | ||
$backdrop = $( '<div id="gra-backdrop" aria-hidden="true"></div>' ); | $backdrop = $('<div id="gra-backdrop" aria-hidden="true"></div>'); | ||
$( 'body' ).append( $backdrop ); | $('body').append($backdrop); | ||
var $toggle = $( [ | var $toggle = $( [ | ||
'<button id="gra-toggle" aria-label="Notes">', | '<button id="gra-toggle" aria-label="Notes">', | ||
| Line 218: | Line 223: | ||
'</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 ); | $panel.hasClass('gra-panel-open') ? closePanel() : openPanel(_activeTab); | ||
} ); | }); | ||
$('#gra-panel-title').text(pageTitle.replace(/_/g,' ').split('/')[0].slice(0,30)); | |||
$( '#gra-panel-title' ) | $tabNotes = $('#gra-tab-notes'); | ||
$tabBookmarks = $('#gra-tab-bookmarks'); | |||
$tabNotes = $( '#gra-tab-notes' ); | $paneNotes = $('#gra-pane-notes'); | ||
$tabBookmarks = $( '#gra-tab-bookmarks' ); | $paneBookmarks= $('#gra-pane-bookmarks'); | ||
$paneNotes = $( '#gra-pane-notes' ); | $ntInput = $('#gra-nt-input'); | ||
$paneBookmarks= $( '#gra-pane-bookmarks' ); | $ntSubmit = $('#gra-nt-submit'); | ||
$ntInput = $( '#gra-nt-input' ); | $bmInput = $('#gra-bm-input'); | ||
$ntSubmit = $( '#gra-nt-submit' ); | $bmSubmit = $('#gra-bm-submit'); | ||
$bmInput = $( '#gra-bm-input' ); | $fbIssueType = $('#gra-fb-issue'); | ||
$bmSubmit = $( '#gra-bm-submit' ); | $fbText = $('#gra-fb-text'); | ||
$fbIssueType = $( '#gra-fb-issue' ); | $fbEmail = $('#gra-fb-email'); | ||
$fbText = $( '#gra-fb-text' ); | $fbSubmit = $('#gra-fb-submit'); | ||
$fbEmail = $( '#gra-fb-email' ); | $fbQuote = $('#gra-fb-quote'); | ||
$fbSubmit = $( '#gra-fb-submit' ); | |||
$fbQuote = $( '#gra-fb-quote' ); | |||
} | } | ||
| Line 247: | Line 250: | ||
function captureSelection() { | function captureSelection() { | ||
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 = sel.getRangeAt(0); | ||
var text = sel.toString().trim(); | var text = sel.toString().trim(); | ||
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 start = range.commonAncestorContainer; | var start = range.commonAncestorContainer; | ||
if ( start.nodeType === 3 ) start = start.parentNode; | if (start.nodeType === 3) start = start.parentNode; | ||
if ( !contentEl.contains( start ) ) return false; | if (!contentEl.contains(start)) return false; | ||
_selText = text; | _selText = text; | ||
_selRange = range.cloneRange(); | _selRange = range.cloneRange(); | ||
| Line 262: | Line 265: | ||
} | } | ||
function | function tryShowActions() { | ||
if ( $fbComposer.hasClass('gra-composer-visible') ) return; | if ($fbComposer.hasClass('gra-composer-visible')) return; | ||
if ( $ntComposer.hasClass('gra-composer-visible') ) return; | if ($ntComposer.hasClass('gra-composer-visible')) return; | ||
if ( $bmComposer.hasClass('gra-composer-visible') ) return; | if ($bmComposer.hasClass('gra-composer-visible')) return; | ||
if ( captureSelection() ) { | if (!captureSelection()) { | ||
_fabSelVer = _selVersion; | hideActions(); | ||
return; | |||
} | |||
_fabSelVer = _selVersion; | |||
if (isMobile()) { | |||
showMobileBar(); | |||
} else { | } else { | ||
showFab(_selRect); | |||
} | } | ||
} | } | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// FAB | // DESKTOP FAB | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
function showFab( rect ) { | function showFab(rect) { | ||
if ( !rect ) return; | if (!rect) return; | ||
var fabW = 46, fabH = 126; | var fabW = 46, fabH = 126; | ||
var top = rect.top + ( rect.height / 2 ) - ( fabH / 2 ); | var top = rect.top + (rect.height/2) - (fabH/2); | ||
var left = rect.right + 10; | var left = rect.right + 10; | ||
if ( left + fabW > window.innerWidth - 8 ) left = rect.left - fabW - 10; | if (left + fabW > window.innerWidth - 8) left = rect.left - fabW - 10; | ||
top = clamp( top, 8, window.innerHeight - fabH - 8 ); | top = clamp(top, 8, window.innerHeight - fabH - 8); | ||
left = clamp( left, 8, window.innerWidth - fabW - 8 ); | left = clamp(left, 8, window.innerWidth - fabW - 8); | ||
$fab.css({ top: top + 'px', left: left + 'px' }).addClass('gra-fab-visible'); | $fab.css({top: top+'px', left: left+'px'}).addClass('gra-fab-visible'); | ||
} | } | ||
| Line 292: | Line 299: | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// | // MOBILE BOTTOM BAR | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
function positionComposer( $el ) { | function showMobileBar() { | ||
if ( !_selRect ) return; | $mobileBar.addClass('gra-mobile-bar-visible'); | ||
} | |||
function hideMobileBar() { | |||
$mobileBar.removeClass('gra-mobile-bar-visible'); | |||
} | |||
function hideActions() { | |||
hideFab(); | |||
hideMobileBar(); | |||
} | |||
// ════════════════════════════════════════════════════════════════════ | |||
// COMPOSER POSITIONING (desktop note/bookmark only) | |||
// ════════════════════════════════════════════════════════════════════ | |||
function positionComposer($el) { | |||
if (isMobile()) { | |||
// On mobile always center — don't anchor to selection | |||
$el.css({top: '', left: '', transform: ''}); | |||
return; | |||
} | |||
if (!_selRect) return; | |||
var W = 340; | var W = 340; | ||
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 + W > window.innerWidth - 8) left = window.innerWidth - W - 8; | ||
left = Math.max( left, 8 ); | left = Math.max(left, 8); | ||
if ( top + 280 > window.innerHeight ) top = _selRect.top - 290; | if (top + 280 > window.innerHeight) top = _selRect.top - 290; | ||
top = Math.max( top, 8 ) | top = Math.max(top, 8); | ||
$el.css({top: top+'px', left: left+'px'}); | |||
$el.css({ top: top + 'px', left: left + 'px' }); | |||
} | } | ||
| Line 312: | Line 340: | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
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 320: | Line 348: | ||
span.className = cssClass; | span.className = cssClass; | ||
span.setAttribute('data-gra-id', id); | span.setAttribute('data-gra-id', id); | ||
range.surroundContents( span ); | range.surroundContents(span); | ||
return span; | return span; | ||
} catch (e) { | } catch(e) { | ||
try { | try { | ||
var frag = range.extractContents(); | var frag = range.extractContents(); | ||
| Line 336: | Line 364: | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// FEEDBACK FLOW | // FEEDBACK FLOW | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
function openFeedbackComposer() { | function openFeedbackComposer() { | ||
hideActions(); | |||
$fbQuote.text(_selText.slice(0,200) + (_selText.length > 200 ? '…' : '')); | |||
$fbQuote.text( _selText.slice(0, 200) + (_selText.length > 200 ? '…' : '') ); | |||
$fbIssueType.val(''); | $fbIssueType.val(''); | ||
$fbText.val(''); | $fbText.val(''); | ||
$fbSubmit.prop('disabled', true); | $fbSubmit.prop('disabled', true); | ||
$( '#gra-fb-status' ).text('').removeClass('gra-fb-ok gra-fb-err'); | $('#gra-fb-status').text('').removeClass('gra-fb-ok gra-fb-err'); | ||
if (currentUserEmail) $fbEmail.val(currentUserEmail); | |||
$fbComposer.css({ top: '', left: '', transform: '' }); | else $fbEmail.val(''); | ||
$fbComposer.css({top:'', left:'', transform:''}); | |||
$fbComposer.addClass('gra-composer-visible'); | $fbComposer.addClass('gra-composer-visible'); | ||
$backdrop.addClass('gra-backdrop-visible'); | $backdrop.addClass('gra-backdrop-visible'); | ||
setTimeout( function() { $fbIssueType.focus(); }, isMobile() ? 300 : 0 ); | setTimeout(function(){ $fbIssueType.focus(); }, isMobile() ? 300 : 0); | ||
} | } | ||
| Line 370: | Line 393: | ||
var email = $fbEmail.val().trim(); | var email = $fbEmail.val().trim(); | ||
var quote = $fbQuote.text(); | var quote = $fbQuote.text(); | ||
if (!issueType) return; | |||
if ( !issueType ) return; | |||
$fbSubmit.prop('disabled', true).text('Sending…'); | $fbSubmit.prop('disabled', true).text('Sending…'); | ||
$( '#gra-fb-status' ).text('').removeClass('gra-fb-ok gra-fb-err'); | $('#gra-fb-status').text('').removeClass('gra-fb-ok gra-fb-err'); | ||
var issueLabels = { | var issueLabels = { | ||
wrong_text: | wrong_text: 'Formatting error', reference_issue: 'Reference issue', | ||
spelling_mistake: 'Spelling mistake', other: 'Other' | |||
spelling_mistake: 'Spelling mistake', | |||
}; | }; | ||
var payload = new FormData(); | var payload = new FormData(); | ||
payload.append( 'issue_type', issueLabels[issueType] || issueType ); | payload.append('issue_type', issueLabels[issueType] || issueType); | ||
payload.append( 'page', pageTitle.replace(/_/g,' ') ); | payload.append('page', pageTitle.replace(/_/g,' ')); | ||
payload.append( 'url', window.location.href ); | payload.append('url', window.location.href); | ||
payload.append( 'selected_text', quote ); | payload.append('selected_text', quote); | ||
payload.append( 'details', details || '' ); | payload.append('details', details || ''); | ||
payload.append( 'user_email', email || currentUserEmail || '' ); | payload.append('user_email', email || currentUserEmail || ''); | ||
payload.append( 'wiki_user', currentUser || 'anonymous' ); | payload.append('wiki_user', currentUser || 'anonymous'); | ||
fetch( '/feedback.php', { method: 'POST', body: payload } ) | fetch('/feedback.php', {method:'POST', body:payload}) | ||
.then( function(r) { return r.json(); } ) | .then(function(r){ return r.json(); }) | ||
.then( function(data) { | .then(function(data){ | ||
if ( data && data.ok ) | if (data && data.ok) showFeedbackSuccess(); | ||
else showFeedbackError(data && data.error ? data.error : 'Could not send.'); | |||
}) | |||
.catch(function(){ showFeedbackError('Network error. Please try again.'); }); | |||
} ) | |||
.catch( function() { | |||
} | } | ||
function showFeedbackSuccess() { | function showFeedbackSuccess() { | ||
$fbSubmit.prop('disabled', false).text('Send'); | $fbSubmit.prop('disabled', false).text('Send'); | ||
$( '#gra-fb-status' ) | $('#gra-fb-status').text('✓ Feedback sent. Thank you!').addClass('gra-fb-ok'); | ||
setTimeout(closeFeedbackComposer, 2500); | |||
setTimeout( closeFeedbackComposer, 2500 ); | |||
} | } | ||
function showFeedbackError( msg ) { | function showFeedbackError(msg) { | ||
$fbSubmit.prop('disabled', false).text('Send'); | $fbSubmit.prop('disabled', false).text('Send'); | ||
$( '#gra-fb-status' ) | $('#gra-fb-status').text('✗ ' + msg).addClass('gra-fb-err'); | ||
} | } | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// NOTE FLOW | // NOTE FLOW | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
function openNoteComposer() { | function openNoteComposer() { | ||
hideActions(); | |||
positionComposer( $ntComposer ); | positionComposer($ntComposer); | ||
$ntComposer.addClass('gra-composer-visible'); | $ntComposer.addClass('gra-composer-visible'); | ||
setTimeout( function() { $ntInput.focus(); }, isMobile() ? 300 : 0 ); | if (isMobile()) $backdrop.addClass('gra-backdrop-visible'); | ||
setTimeout(function(){ $ntInput.focus(); }, isMobile() ? 300 : 0); | |||
} | } | ||
function closeNoteComposer() { | function closeNoteComposer() { | ||
$ntComposer.removeClass('gra-composer-visible'); | $ntComposer.removeClass('gra-composer-visible'); | ||
$backdrop.removeClass('gra-backdrop-visible'); | |||
$ntInput.val(''); | $ntInput.val(''); | ||
$ntSubmit.prop('disabled', true); | $ntSubmit.prop('disabled', true); | ||
| Line 442: | Line 454: | ||
function submitNote() { | function submitNote() { | ||
var text = $ntInput.val().trim(); | var text = $ntInput.val().trim(); | ||
if ( !text ) return; | if (!text) return; | ||
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-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); | ||
_notes.push({id:id, ts:ts, quote:quote, text:text}); | |||
persistNotes(); | persistNotes(); | ||
persistNoteHighlight( id, quote ); | persistNoteHighlight(id, quote); | ||
renderNoteCards(); | renderNoteCards(); | ||
closeNoteComposer(); | closeNoteComposer(); | ||
| Line 458: | Line 469: | ||
function persistNotes() { | function persistNotes() { | ||
try { localStorage.setItem( NT_LS_KEY, JSON.stringify(_notes) ); } catch(e){} | try { localStorage.setItem(NT_LS_KEY, JSON.stringify(_notes)); } catch(e){} | ||
} | } | ||
function loadNotes() { | function loadNotes() { | ||
try { | try { var r = localStorage.getItem(NT_LS_KEY); if (r) _notes = JSON.parse(r)||[]; } catch(e){} | ||
} | } | ||
| Line 473: | Line 480: | ||
function openBookmarkComposer() { | function openBookmarkComposer() { | ||
hideActions(); | |||
positionComposer( $bmComposer ); | positionComposer($bmComposer); | ||
$bmComposer.addClass('gra-composer-visible'); | $bmComposer.addClass('gra-composer-visible'); | ||
setTimeout( function() { $bmInput.focus(); }, isMobile() ? 300 : 0 ); | if (isMobile()) $backdrop.addClass('gra-backdrop-visible'); | ||
setTimeout(function(){ $bmInput.focus(); }, isMobile() ? 300 : 0); | |||
} | } | ||
function closeBookmarkComposer() { | function closeBookmarkComposer() { | ||
$bmComposer.removeClass('gra-composer-visible'); | $bmComposer.removeClass('gra-composer-visible'); | ||
$backdrop.removeClass('gra-backdrop-visible'); | |||
$bmInput.val(''); | $bmInput.val(''); | ||
_selRange = null; _selText = ''; _selRect = null; | _selRange = null; _selText = ''; _selRect = null; | ||
| Line 486: | Line 495: | ||
function submitBookmark() { | function submitBookmark() { | ||
var name = $bmInput.val().trim() || ('Bookmark ' + (_bookmarks.length + 1)); | var name = $bmInput.val().trim() || ('Bookmark ' + (_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'); | 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); } | ||
_bookmarks.push({ id:id, name:name, quote:quote, ts:nowIso() }); | _bookmarks.push({id:id, name:name, quote:quote, ts:nowIso()}); | ||
persistBookmarks(); | persistBookmarks(); | ||
renderBookmarkCards(); | renderBookmarkCards(); | ||
| Line 498: | Line 507: | ||
} | } | ||
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 | var p = span.parentNode; | ||
while (span.firstChild) | while (span.firstChild) p.insertBefore(span.firstChild, span); | ||
p.removeChild(span); | |||
} | } | ||
persistBookmarks(); renderBookmarkCards(); | persistBookmarks(); renderBookmarkCards(); | ||
| Line 546: | Line 555: | ||
function renderNoteCards() { | function renderNoteCards() { | ||
if (!_notes.length) { | if (!_notes.length) { | ||
$paneNotes.html('<div class="gra-empty-state">No notes yet.<br>Select text and | $paneNotes.html('<div class="gra-empty-state">No notes yet.<br>Select text and tap ✎ to add one.</div>'); | ||
return; | return; | ||
} | } | ||
var html = ''; | var html = ''; | ||
_notes.slice().reverse().forEach(function(n){ | _notes.slice().reverse().forEach(function(n){ | ||
html += '<div class="gra-note-card" data-gra-id="' + esc(n.id) + '">' | html += '<div class="gra-note-card" data-gra-id="'+esc(n.id)+'">' | ||
+ '<div class="gra-card-header">' | + '<div class="gra-card-header">' | ||
+ '<div class="gra-avatar">✎</div>' | + '<div class="gra-avatar">✎</div>' | ||
+ '<div class="gra-card-meta">' | + '<div class="gra-card-meta">' | ||
+ (n.ts ? '<div class="gra-card-ts">' + esc(fmtTs(n.ts)) + '</div>' : '') | + (n.ts ? '<div class="gra-card-ts">'+esc(fmtTs(n.ts))+'</div>' : '') | ||
+ '</div>' | + '</div>' | ||
+ '<button class="gra-note-del" data-del-id="' + esc(n.id) + '" title="Delete">×</button>' | + '<button class="gra-note-del" data-del-id="'+esc(n.id)+'" title="Delete">×</button>' | ||
+ '</div>' | + '</div>' | ||
+ (n.quote ? '<div class="gra-card-quote">' + esc(n.quote) + '</div>' : '') | + (n.quote ? '<div class="gra-card-quote">'+esc(n.quote)+'</div>' : '') | ||
+ '<div class="gra-card-text">' + esc(n.text) + '</div>' | + '<div class="gra-card-text">'+esc(n.text)+'</div>' | ||
+ '</div>'; | + '</div>'; | ||
}); | }); | ||
| Line 566: | Line 575: | ||
} | } | ||
function deleteNote( id ) { | function deleteNote(id) { | ||
_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) { | ||
var | var p = span.parentNode; | ||
while (span.firstChild) | while (span.firstChild) p.insertBefore(span.firstChild, span); | ||
p.removeChild(span); | |||
} | } | ||
try { | try { | ||
var s = JSON.parse(localStorage.getItem(NT_LS_KEY+'_hl')||'[]'); | var s = JSON.parse(localStorage.getItem(NT_LS_KEY+'_hl')||'[]'); | ||
| Line 580: | Line 588: | ||
localStorage.setItem(NT_LS_KEY+'_hl', JSON.stringify(s)); | localStorage.setItem(NT_LS_KEY+'_hl', JSON.stringify(s)); | ||
} catch(e){} | } catch(e){} | ||
persistNotes(); | persistNotes(); renderNoteCards(); | ||
} | } | ||
function renderBookmarkCards() { | function renderBookmarkCards() { | ||
if (!_bookmarks.length) { | if (!_bookmarks.length) { | ||
$paneBookmarks.html('<div class="gra-empty-state">No bookmarks yet.<br>Select text and | $paneBookmarks.html('<div class="gra-empty-state">No bookmarks yet.<br>Select text and tap 🔖 to save one.</div>'); | ||
return; | return; | ||
} | } | ||
var html = ''; | var html = ''; | ||
_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" aria-hidden="true"></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="Remove">×</button>' | + '<button class="gra-bookmark-del" data-del-id="'+esc(b.id)+'" title="Remove">×</button>' | ||
+ '</div>'; | + '</div>'; | ||
}); | }); | ||
| Line 608: | Line 615: | ||
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; | ||
el.scrollIntoView({ behavior:'smooth', block:'center' }); | el.scrollIntoView({behavior:'smooth', block:'center'}); | ||
el.classList.add('gra-hl-active'); | el.classList.add('gra-hl-active'); | ||
setTimeout(function(){ el.classList.remove('gra-hl-active'); }, 2000); | setTimeout(function(){ el.classList.remove('gra-hl-active'); }, 2000); | ||
| Line 621: | Line 628: | ||
function wireEvents() { | function wireEvents() { | ||
// ── Desktop | // ── Desktop mouseup ─────────────────────────────────────────── | ||
$( document ).on('mouseup | $(document).on('mouseup', function(e){ | ||
if ( | if (e.button !== 0) return; | ||
setTimeout( | if (isMobile()) return; // mobile handled separately | ||
setTimeout(tryShowActions, 20); | |||
}); | }); | ||
// ── Mobile: selectionchange | // ── Mobile: selectionchange (debounced 600ms) ───────────────── | ||
var | // We wait longer than v3 so the browser's own copy menu has time | ||
// to appear first — user can still copy, THEN our bar slides up. | |||
var _selTimer = null; | |||
document.addEventListener('selectionchange', function() { | document.addEventListener('selectionchange', function() { | ||
_selVersion++; | _selVersion++; | ||
clearTimeout( | clearTimeout(_selTimer); | ||
var v = _selVersion; | var v = _selVersion; | ||
_selTimer = setTimeout(function(){ | |||
if ( v !== _selVersion ) return; | if (v !== _selVersion) return; | ||
if ( _fabSelVer === v ) return; | if (_fabSelVer === v) return; | ||
if (!isMobile()) return; // desktop uses mouseup | |||
}, | tryShowActions(); | ||
}, 600); // 600ms — after browser copy menu appears | |||
}); | }); | ||
// ── | // ── Click outside → hide actions ────────────────────────────── | ||
document. | $(document).on('mousedown touchstart', function(e){ | ||
if ( $fab[0] && $fab[0].contains( | var t = e.target; | ||
if ( $fbComposer[0] && $fbComposer[0].contains( | if ($fab[0] && $fab[0].contains(t)) return; | ||
if ( $ntComposer[0] && $ntComposer[0].contains( | if ($mobileBar[0] && $mobileBar[0].contains(t)) return; | ||
if ( $bmComposer[0] && $bmComposer[0].contains( | if ($fbComposer[0] && $fbComposer[0].contains(t)) return; | ||
if ($ntComposer[0] && $ntComposer[0].contains(t)) return; | |||
if ($bmComposer[0] && $bmComposer[0].contains(t)) return; | |||
hideActions(); | |||
}); | |||
// ── | // ── Desktop FAB buttons ─────────────────────────────────────── | ||
$( | $('#gra-fab-feedback').on('click', function(e){ | ||
e.preventDefault(); e.stopPropagation(); | |||
if ( | if (!_selRange && !captureSelection()) return; | ||
if ( | openFeedbackComposer(); | ||
}); | |||
if ( | $('#gra-fab-note').on('click', function(e){ | ||
e.preventDefault(); e.stopPropagation(); | |||
if (!_selRange && !captureSelection()) return; | |||
openNoteComposer(); | |||
}); | |||
$('#gra-fab-bookmark').on('click', function(e){ | |||
e.preventDefault(); e.stopPropagation(); | |||
if (!_selRange && !captureSelection()) return; | |||
openBookmarkComposer(); | |||
}); | }); | ||
// ── | // ── Mobile bottom bar buttons ───────────────────────────────── | ||
$( '#gra- | $('#gra-mob-feedback').on('click touchend', function(e){ | ||
e.preventDefault(); e.stopPropagation(); | e.preventDefault(); e.stopPropagation(); | ||
if ( !_selRange && !captureSelection() ) return; | hideMobileBar(); | ||
if (!_selRange && !captureSelection()) return; | |||
openFeedbackComposer(); | openFeedbackComposer(); | ||
}); | }); | ||
$( '#gra- | $('#gra-mob-note').on('click touchend', function(e){ | ||
e.preventDefault(); e.stopPropagation(); | e.preventDefault(); e.stopPropagation(); | ||
if ( !_selRange && !captureSelection() ) return; | hideMobileBar(); | ||
if (!_selRange && !captureSelection()) return; | |||
openNoteComposer(); | openNoteComposer(); | ||
}); | }); | ||
$( '#gra- | $('#gra-mob-bookmark').on('click touchend', function(e){ | ||
e.preventDefault(); e.stopPropagation(); | e.preventDefault(); e.stopPropagation(); | ||
if ( !_selRange && !captureSelection() ) return; | hideMobileBar(); | ||
if (!_selRange && !captureSelection()) return; | |||
openBookmarkComposer(); | openBookmarkComposer(); | ||
}); | |||
$('#gra-mob-dismiss').on('click touchend', function(e){ | |||
e.preventDefault(); e.stopPropagation(); | |||
hideMobileBar(); | |||
// Clear selection visually | |||
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', function(){ | $('#gra-fb-cancel, #gra-fb-close').on('click', function(){ | ||
closeFeedbackComposer | closeFeedbackComposer(); | ||
}); | }); | ||
$fbSubmit.on('click', submitFeedback); | $fbSubmit.on('click', submitFeedback); | ||
$fbText.on('keydown', function(e){ | $fbText.on('keydown', function(e){ | ||
if (e.key==='Escape') | 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', | $('#gra-nt-cancel').on('click', closeNoteComposer); | ||
$ntSubmit.on('click', submitNote); | $ntSubmit.on('click', submitNote); | ||
$ntInput.on('keydown', function(e){ | $ntInput.on('keydown', function(e){ | ||
if ((e.ctrlKey||e.metaKey) && e.key==='Enter') submitNote(); | if ((e.ctrlKey||e.metaKey) && e.key==='Enter') submitNote(); | ||
if (e.key==='Escape') | if (e.key==='Escape') closeNoteComposer(); | ||
}); | }); | ||
// ── Bookmark composer ───────────────────────────────────────── | // ── Bookmark composer ───────────────────────────────────────── | ||
$( '#gra-bm-cancel' ).on('click', | $('#gra-bm-cancel').on('click', closeBookmarkComposer); | ||
$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') | if (e.key==='Escape') closeBookmarkComposer(); | ||
}); | }); | ||
// ── Panel ───────────────────────────────────────────────────── | // ── Panel ───────────────────────────────────────────────────── | ||
$( '#gra-panel-close' ).on('click', closePanel); | $('#gra-panel-close').on('click', closePanel); | ||
$backdrop.on('click | $backdrop.on('click touchend', function(e){ | ||
if ( $fbComposer.hasClass('gra-composer-visible') ) closeFeedbackComposer(); | e.preventDefault(); | ||
if ($fbComposer.hasClass('gra-composer-visible')) closeFeedbackComposer(); | |||
else if ($ntComposer.hasClass('gra-composer-visible')) closeNoteComposer(); | |||
else if ($bmComposer.hasClass('gra-composer-visible')) closeBookmarkComposer(); | |||
else closePanel(); | else closePanel(); | ||
}); | }); | ||
| Line 716: | Line 748: | ||
$tabBookmarks.on('click', function(){ switchTab('bookmarks'); }); | $tabBookmarks.on('click', function(){ switchTab('bookmarks'); }); | ||
$paneNotes.on('click', '.gra-note-card', function(e){ | $paneNotes.on('click', '.gra-note-card', function(e){ | ||
if ($( e.target ).hasClass('gra-note-del')) return; | if ($(e.target).hasClass('gra-note-del')) return; | ||
var id = $( this ).attr('data-gra-id'); | var id = $(this).attr('data-gra-id'); | ||
if (id) { closePanel(); scrollToHighlight(id); } | if (id) { closePanel(); scrollToHighlight(id); } | ||
}); | }); | ||
$paneNotes.on('click', '.gra-note-del', function(e){ | $paneNotes.on('click', '.gra-note-del', function(e){ | ||
e.stopPropagation(); | e.stopPropagation(); | ||
var id = $( this ).attr('data-del-id'); | var id = $(this).attr('data-del-id'); | ||
if (id) deleteNote(id); | if (id) deleteNote(id); | ||
}); | }); | ||
$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) { closePanel(); scrollToHighlight(id); } | if (id) { closePanel(); scrollToHighlight(id); } | ||
}); | }); | ||
$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); | ||
}); | }); | ||
$(CONTENT_SEL).on('click', '.gra-note-highlight', function(){ | |||
$( CONTENT_SEL ).on('click', '.gra-note-highlight', function(){ | var id = $(this).attr('data-gra-id'); | ||
var id = $( this ).attr('data-gra-id'); | |||
openPanel('notes'); | openPanel('notes'); | ||
setTimeout(function(){ | setTimeout(function(){ | ||
| Line 754: | Line 781: | ||
}, 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(){ | ||
| Line 763: | Line 790: | ||
}); | }); | ||
$(document).on('keydown', function(e){ | |||
$( document ).on('keydown', function(e){ | |||
if (e.key !== 'Escape') return; | if (e.key !== 'Escape') return; | ||
if ($fbComposer.hasClass('gra-composer-visible')) | if ($fbComposer.hasClass('gra-composer-visible')) closeFeedbackComposer(); | ||
else if ($ntComposer.hasClass('gra-composer-visible')) | else if ($ntComposer.hasClass('gra-composer-visible')) closeNoteComposer(); | ||
else if ($bmComposer.hasClass('gra-composer-visible')) | else if ($bmComposer.hasClass('gra-composer-visible')) closeBookmarkComposer(); | ||
else closePanel(); | else closePanel(); | ||
}); | }); | ||
| Line 774: | Line 800: | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
// RESTORE HIGHLIGHTS | // RESTORE HIGHLIGHTS | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
| Line 846: | Line 872: | ||
// ════════════════════════════════════════════════════════════════════ | // ════════════════════════════════════════════════════════════════════ | ||
$( function () { | $(function() { | ||
// Detect mobile once on load | |||
_mobile = window.innerWidth < 768 || 'ontouchstart' in window; | |||
window.addEventListener('resize', function(){ | |||
_mobile = window.innerWidth < 768 || 'ontouchstart' in window; | |||
}); | |||
buildDom(); | buildDom(); | ||
wireEvents(); | wireEvents(); | ||