Index: src/default.css ================================================================== --- src/default.css +++ src/default.css @@ -51,10 +51,19 @@ } tr.timelineCurrent td { border-radius: 0; border-width: 0; } +.timelineFocused { + background-image: url("data:image/svg+xml,%3Csvg \ +xmlns='http://www.w3.org/2000/svg' viewBox='0,0,1,1'%3E%3Cpath \ +style='fill:orange;opacity:0.5' d='M0,0h1v1h-1z'/%3E%3C/svg%3E"); +/*Note: IE requires explicit declarations for the next three properties.*/ + background-position: top left; + background-repeat: repeat repeat; + background-size: 64px 64px; +} span.timelineLeaf { font-weight: bold; } span.timelineHistDsp { font-weight: bold; Index: src/graph.js ================================================================== --- src/graph.js +++ src/graph.js @@ -134,11 +134,11 @@ stopCloseTimer(); tooltipObj.style.display = "none"; tooltipInfo.ixActive = -1; tooltipInfo.idNodeActive = 0; } -document.body.onunload = hideGraphTooltip +window.onpageshow = window.onpagehide = hideGraphTooltip; function stopDwellTimer(){ if(tooltipInfo.idTimer!=0){ clearTimeout(tooltipInfo.idTimer); tooltipInfo.idTimer = 0; } @@ -798,5 +798,203 @@ var txJson = dataObj.textContent || dataObj.innerText; var tx = JSON.parse(txJson); TimelineGraph(tx); } }()); + +/* +** Timeline keyboard navigation shortcuts: +** +** N - Select next (newer) entry. +** M - Select previous (older) entry. +** J - View timeline of selected entry. +** K - View details of selected entry. +** L - Disable keyboard navigation mode. +** +** When navigating to a page with a timeline display, such as /timeline, /info, +** or /finfo, keyboard navigation mode needs to be "activated" first, i.e. if no +** timeline entry is focused yet, pressing any of the listed keys (except L) +** sets the visual focus indicator to the highlighted or current (check-out) +** entry if available, or to the topmost entry otherwise. A session cookie[0] is +** used to direct pages loaded in the future to enable keyboard navigation mode +** and automatically set the focus indicator to the highlighted, current, or +** topmost entry. Pressing N and M on the /timeline page while the topmost or +** bottommost entry is focused loads the next or previous page if available, +** similar to the [↑ More] and [↓ More] links. Pressing L disables keyboard +** navigation, i.e. removes the focus indicator and deletes the session cookie. +** When navigating backwards or forwards in browser history, the focused entry +** is restored using a hidden[1] input field. +** +** [0]: The lifetime and values of cookies can be tracked on the /cookies page. +** A session cookie is preferred over other storage APIs because Fossil already +** requires cookies to be enabled for reasonable functionality, and it's more +** likely that other storage APIs are blocked by users for privacy reasons, for +** example. +** [1]: This feature only works with a normal (text) input field hidden by CSS +** styles, instead of a true hidden (by type) input field, but according to MDN, +** screen readers should ignore it even without an aria-hidden="true" attribute +** (which is even discouraged for hidden by CSS elements). Also, this feature +** breaks if disabled[=true] or tabindex="-1" attributes are added to the input +** field, or (in FF) if page unload handlers are present. +** +** Ideas and TODOs: +** +** o Shortcut to select the topmost or bottommost entry, either by separate +** key, or with modifiers (SHIFT+N, SHIFT+M)? +** o Shortcut to toggle the tick-mark for the focused check-in node. +** o Shortcuts to copy branch name or hash of the focused entry to clipboard. +** o Shortcut to put the focus indicator to the default item(s), in (cyclic) +** order ticked → highlighted → check-out → ticked → ... +** o Improve scrolling the focused element into view for browsers without the +** Element.scrollIntoViewIfNeeded() function, maybe with a Polyfill, or +** something similar to the scrollToSelected() function in this source file. +*/ +(function(){ + window.addEventListener('load',function(){ + function focusDefaultId(){ + var tn = document.querySelector('.timelineSelected .tl-nodemark') || + document.querySelector('.timelineCurrent .tl-nodemark'); + return tn ? tn.id : 'm1'; + } + function focusFirstId(id){ + return 'm1'; + } + function focusLastId(id){ + var el = document.getElementsByClassName('tl-nodemark'); + var tn = el ? el[el.length-1] : null; + return tn ? tn.id : id; + } + function focusNextId(id,dx){ + if( dx<-1 ) return focusFirstId(id); + if( dx>+1 ) return focusLastId(id); + var m = /^m(\d+)$/.exec(id); + return m!==null ? 'm' + (parseInt(m[1]) + dx) : null; + } + function timelineGetDataBlock(i){ + var tb = document.getElementById('timeline-data-' + i); + return tb ? JSON.parse(tb.textContent || tb.innerText) : null; + } + function timelineGetRowInfo(id){ + var ti; + for(var i=0; ti=timelineGetDataBlock(i); i++){ + for( var k=0; k0 ? 'prev' : 'next' )); + if( btn ) btn.click(); + return; + } + } + else if ( !id ) id = focusDefaultId(); + focusCacheSet(id); + focusVisualize(id,true); + }/*,true*/); + window.addEventListener('pageshow',function(evt){ + var id = focusCacheGet(); + if( !id || !focusVisualize(id,false) ){ + if( document.cookie.match(/fossil_timeline_kbnav=1/) ){ + id = focusDefaultId(); + focusCacheSet(id); + focusVisualize(id,false); + } + } + },false); + },false); +}()); Index: src/timeline.c ================================================================== --- src/timeline.c +++ src/timeline.c @@ -2729,18 +2729,18 @@ if( zError ){ @

%h(zError)

} if( zNewerButton ){ - @ %z(chref("button","%s",zNewerButton))%h(zNewerButtonLabel)\ + @ %z(chref("button tl-button-next","%s",zNewerButton))%h(zNewerButtonLabel)\ @  ↑ } www_print_timeline(&q, tmFlags, zThisUser, zThisTag, zBrName, selectedRid, secondaryRid, 0); db_finalize(&q); if( zOlderButton ){ - @ %z(chref("button","%s",zOlderButton))%h(zOlderButtonLabel)\ + @ %z(chref("button tl-button-prev","%s",zOlderButton))%h(zOlderButtonLabel)\ @  ↓ } document_emit_js(/*handles pikchrs rendered above*/); style_finish_page(); }