@@ -612,6 +612,8 @@ const state = {
612612 focusedChapterIndex : null ,
613613 annotations : [ ] ,
614614 activeAnnotationId : null ,
615+ annotationRange : null ,
616+ annotationStatusTimer : null ,
615617 observer : null ,
616618 guidePromise : null ,
617619} ;
@@ -627,6 +629,7 @@ const els = {
627629 plannerView : document . querySelector ( "#planner-view" ) ,
628630 scheduleView : document . querySelector ( "#schedule-view" ) ,
629631 readerView : document . querySelector ( "#reader-view" ) ,
632+ readerShell : document . querySelector ( ".reader-shell" ) ,
630633 plannerNav : document . querySelector ( "#planner-nav" ) ,
631634 readerNav : document . querySelector ( "#reader-nav" ) ,
632635 modeButtons : [ ...document . querySelectorAll ( ".view-toggle button[data-mode]" ) ] ,
@@ -704,6 +707,7 @@ const els = {
704707 nextChapterTitle : document . querySelector ( "#next-chapter-title" ) ,
705708 annotationKicker : document . querySelector ( "#annotation-kicker" ) ,
706709 annotationTitle : document . querySelector ( "#annotation-title" ) ,
710+ annotationPopover : document . querySelector ( "#annotation-popover" ) ,
707711 annotationStatus : document . querySelector ( "#annotation-status" ) ,
708712 annotationList : document . querySelector ( "#annotation-list" ) ,
709713 highlightButton : document . querySelector ( "#highlight-button" ) ,
@@ -984,6 +988,9 @@ function setLanguageChrome(lang) {
984988 els . nextChapterKicker . textContent = text . nextChapterKicker ;
985989 els . annotationKicker . textContent = text . annotationKicker ;
986990 els . annotationTitle . textContent = text . annotationTitle ;
991+ els . annotationPopover . setAttribute ( "aria-label" , text . annotationTitle ) ;
992+ clearAnnotationStatusTimer ( ) ;
993+ els . annotationStatus . classList . remove ( "is-visible" ) ;
987994 els . annotationStatus . textContent = text . annotationStatus ;
988995 [
989996 [ els . highlightButton , text . annotationHighlight ] ,
@@ -1008,6 +1015,7 @@ function setLanguageChrome(lang) {
10081015function setMode ( mode , shouldUpdateUrl = true ) {
10091016 const modes = new Set ( [ "planner" , "schedule" , "reader" ] ) ;
10101017 state . mode = modes . has ( mode ) ? mode : "planner" ;
1018+ hideAnnotationPopover ( ) ;
10111019 els . body . dataset . mode = state . mode ;
10121020 els . plannerView . hidden = state . mode !== "planner" ;
10131021 els . scheduleView . hidden = state . mode !== "schedule" ;
@@ -1168,6 +1176,7 @@ function applyReaderFocus() {
11681176function setReaderFocus ( chapterIndex ) {
11691177 const next = Number ( chapterIndex ) ;
11701178 state . focusedChapterIndex = Number . isInteger ( next ) && next >= 1 && next <= CHAPTER_COUNT ? next : null ;
1179+ hideAnnotationPopover ( ) ;
11711180 applyReaderFocus ( ) ;
11721181 renderToc ( ) ;
11731182 observeHeadings ( ) ;
@@ -1179,10 +1188,111 @@ function cssEscape(value) {
11791188 return window . CSS ?. escape ? CSS . escape ( value ) : String ( value ) . replace ( / [ " \\ ] / g, "\\$&" ) ;
11801189}
11811190
1191+ function clearAnnotationStatusTimer ( ) {
1192+ if ( state . annotationStatusTimer ) {
1193+ window . clearTimeout ( state . annotationStatusTimer ) ;
1194+ state . annotationStatusTimer = null ;
1195+ }
1196+ }
1197+
11821198function setAnnotationStatus ( message ) {
11831199 if ( els . annotationStatus ) {
1184- els . annotationStatus . textContent = message || copy [ state . lang ] . annotationStatus ;
1200+ clearAnnotationStatusTimer ( ) ;
1201+ const fallback = copy [ state . lang ] . annotationStatus ;
1202+ const nextMessage = message || fallback ;
1203+ els . annotationStatus . textContent = nextMessage ;
1204+ els . annotationStatus . classList . toggle ( "is-visible" , Boolean ( message ) ) ;
1205+
1206+ if ( message ) {
1207+ state . annotationStatusTimer = window . setTimeout ( ( ) => {
1208+ els . annotationStatus . classList . remove ( "is-visible" ) ;
1209+ els . annotationStatus . textContent = fallback ;
1210+ state . annotationStatusTimer = null ;
1211+ } , 2200 ) ;
1212+ }
1213+ }
1214+ }
1215+
1216+ function selectionRangeInArticle ( ) {
1217+ const selection = window . getSelection ( ) ;
1218+ if ( ! selection || selection . rangeCount === 0 || selection . isCollapsed ) return null ;
1219+
1220+ const range = selection . getRangeAt ( 0 ) ;
1221+ if ( ! range . toString ( ) . trim ( ) ) return null ;
1222+ if ( ! els . article . contains ( range . commonAncestorContainer ) ) return null ;
1223+
1224+ return range ;
1225+ }
1226+
1227+ function selectedRangeRect ( range ) {
1228+ const rects = [ ...range . getClientRects ( ) ] . filter ( ( rect ) => rect . width > 0 && rect . height > 0 ) ;
1229+ return rects [ 0 ] || range . getBoundingClientRect ( ) ;
1230+ }
1231+
1232+ function hideAnnotationPopover ( options = { } ) {
1233+ if ( els . annotationPopover ) {
1234+ els . annotationPopover . hidden = true ;
1235+ els . annotationPopover . classList . remove ( "is-below" ) ;
1236+ }
1237+
1238+ if ( options . keepRange ) return ;
1239+ state . annotationRange = null ;
1240+ }
1241+
1242+ function positionAnnotationPopover ( range ) {
1243+ if ( ! els . annotationPopover || ! range ) return ;
1244+
1245+ const rect = selectedRangeRect ( range ) ;
1246+ if ( ! rect || ( rect . width <= 0 && rect . height <= 0 ) ) {
1247+ hideAnnotationPopover ( ) ;
1248+ return ;
1249+ }
1250+
1251+ els . annotationPopover . hidden = false ;
1252+ els . annotationPopover . classList . remove ( "is-below" ) ;
1253+
1254+ const popoverRect = els . annotationPopover . getBoundingClientRect ( ) ;
1255+ const inset = 10 ;
1256+ const left = Math . min (
1257+ window . innerWidth - popoverRect . width / 2 - inset ,
1258+ Math . max ( popoverRect . width / 2 + inset , rect . left + rect . width / 2 ) ,
1259+ ) ;
1260+ const shouldPlaceBelow = rect . top < popoverRect . height + 18 ;
1261+ const top = shouldPlaceBelow ? rect . bottom : rect . top ;
1262+
1263+ els . annotationPopover . classList . toggle ( "is-below" , shouldPlaceBelow ) ;
1264+ els . annotationPopover . style . left = `${ left } px` ;
1265+ els . annotationPopover . style . top = `${ Math . max ( inset , top ) } px` ;
1266+ }
1267+
1268+ function updateAnnotationPopoverFromSelection ( ) {
1269+ if ( state . mode !== "reader" || els . article . classList . contains ( "loading" ) ) {
1270+ hideAnnotationPopover ( ) ;
1271+ return ;
1272+ }
1273+
1274+ const range = selectionRangeInArticle ( ) ;
1275+ if ( ! range ) {
1276+ hideAnnotationPopover ( ) ;
1277+ return ;
1278+ }
1279+
1280+ state . annotationRange = range . cloneRange ( ) ;
1281+ positionAnnotationPopover ( range ) ;
1282+ }
1283+
1284+ function scheduleAnnotationPopoverUpdate ( ) {
1285+ window . setTimeout ( updateAnnotationPopoverFromSelection , 0 ) ;
1286+ }
1287+
1288+ function annotationRangeForAction ( ) {
1289+ const liveRange = selectionRangeInArticle ( ) ;
1290+ if ( liveRange ) {
1291+ state . annotationRange = liveRange . cloneRange ( ) ;
1292+ return liveRange . cloneRange ( ) ;
11851293 }
1294+
1295+ return state . annotationRange ? state . annotationRange . cloneRange ( ) : null ;
11861296}
11871297
11881298function annotationStyleLabel ( style ) {
@@ -1377,15 +1487,9 @@ function renderAnnotationList() {
13771487 const text = copy [ state . lang ] ;
13781488 const annotations = annotationsForCurrentRange ( ) . sort ( ( a , b ) => String ( b . createdAt ) . localeCompare ( String ( a . createdAt ) ) ) ;
13791489 els . annotationList . innerHTML = "" ;
1490+ els . readerShell ?. classList . toggle ( "has-annotations" , annotations . length > 0 ) ;
13801491
13811492 if ( ! annotations . length ) {
1382- const empty = document . createElement ( "div" ) ;
1383- empty . className = "annotation-card" ;
1384- const message = document . createElement ( "p" ) ;
1385- message . className = "annotation-card-comment" ;
1386- message . textContent = text . annotationEmpty ;
1387- empty . append ( message ) ;
1388- els . annotationList . append ( empty ) ;
13891493 return ;
13901494 }
13911495
@@ -1453,13 +1557,12 @@ function renderAnnotations() {
14531557
14541558function createAnnotation ( style ) {
14551559 const text = copy [ state . lang ] ;
1456- const selection = window . getSelection ( ) ;
1457- if ( ! selection || selection . rangeCount === 0 || selection . isCollapsed ) {
1560+ const range = annotationRangeForAction ( ) ;
1561+ if ( ! range || range . collapsed ) {
14581562 setAnnotationStatus ( text . annotationNeedSelection ) ;
14591563 return ;
14601564 }
14611565
1462- const range = selection . getRangeAt ( 0 ) ;
14631566 if ( ! els . article . contains ( range . commonAncestorContainer ) ) {
14641567 setAnnotationStatus ( text . annotationOutside ) ;
14651568 return ;
@@ -1514,7 +1617,9 @@ function createAnnotation(style) {
15141617 state . annotations . push ( annotation ) ;
15151618 state . activeAnnotationId = annotation . id ;
15161619 writeAnnotations ( ) ;
1517- selection . removeAllRanges ( ) ;
1620+ const selection = window . getSelection ( ) ;
1621+ selection ?. removeAllRanges ( ) ;
1622+ hideAnnotationPopover ( ) ;
15181623 renderAnnotations ( ) ;
15191624 setAnnotationStatus ( text . annotationSaved ) ;
15201625}
@@ -2648,13 +2753,26 @@ els.exportMatrixPdf.addEventListener("click", exportSchedulePdf);
26482753els . resetMatrix . addEventListener ( "click" , resetMatrixProgress ) ;
26492754els . search . addEventListener ( "input" , renderToc ) ;
26502755els . top . addEventListener ( "click" , ( ) => window . scrollTo ( { top : 0 , behavior : "smooth" } ) ) ;
2756+ els . annotationPopover . addEventListener ( "mousedown" , ( event ) => event . preventDefault ( ) ) ;
2757+ els . annotationPopover . addEventListener ( "click" , ( event ) => event . stopPropagation ( ) ) ;
26512758els . highlightButton . addEventListener ( "click" , ( ) => createAnnotation ( "highlight" ) ) ;
26522759els . underlineButton . addEventListener ( "click" , ( ) => createAnnotation ( "underline" ) ) ;
26532760els . commentButton . addEventListener ( "click" , ( ) => createAnnotation ( "comment" ) ) ;
2654- window . addEventListener ( "scroll" , updateReadingProgress , { passive : true } ) ;
2761+ document . addEventListener ( "selectionchange" , scheduleAnnotationPopoverUpdate ) ;
2762+ document . addEventListener ( "mouseup" , scheduleAnnotationPopoverUpdate ) ;
2763+ document . addEventListener ( "keyup" , scheduleAnnotationPopoverUpdate ) ;
2764+ document . addEventListener ( "pointerdown" , ( event ) => {
2765+ if ( event . target . closest ( "#annotation-popover" ) || event . target . closest ( "#guide-content" ) ) return ;
2766+ hideAnnotationPopover ( ) ;
2767+ } ) ;
2768+ window . addEventListener ( "scroll" , ( ) => {
2769+ updateReadingProgress ( ) ;
2770+ hideAnnotationPopover ( ) ;
2771+ } , { passive : true } ) ;
26552772window . addEventListener ( "resize" , ( ) => {
26562773 updateFixedChromeMetrics ( ) ;
26572774 updateReadingProgress ( ) ;
2775+ hideAnnotationPopover ( ) ;
26582776} ) ;
26592777window . addEventListener ( "load" , updateFixedChromeMetrics ) ;
26602778window . visualViewport ?. addEventListener ( "resize" , updateFixedChromeMetrics ) ;
0 commit comments