Index: skins/ardoise/css.txt ================================================================== --- skins/ardoise/css.txt +++ skins/ardoise/css.txt @@ -997,10 +997,19 @@ text-align: left; padding: .75em; border-radius: 5px; border: dashed #ff8000 } +.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:white;opacity:0.2' d='M0,0h1v1h-1z'/%3E%3C/svg%3E") !important; +/*Note: IE requires explicit declarations for the next three properties.*/ + background-position: top left; + background-repeat: repeat repeat; + background-size: 64px 64px; +} .timelineModernCell[id], .timelineColumnarCell[id], .timelineDetailCell[id] { background-color: #000 } .tl-canvas { margin: 0 6px 0 10px Index: skins/darkmode/css.txt ================================================================== --- skins/darkmode/css.txt +++ skins/darkmode/css.txt @@ -353,10 +353,19 @@ text-align: left;*/ padding: .75em; border-radius: 5px; border: dashed #ff8000 } +.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:white;opacity:0.2' d='M0,0h1v1h-1z'/%3E%3C/svg%3E") !important; +/*Note: IE requires explicit declarations for the next three properties.*/ + background-position: top left; + background-repeat: repeat repeat; + background-size: 64px 64px; +} .timelineModernCell[id], .timelineColumnarCell[id], .timelineDetailCell[id] { background-color: inherit;/*#000*/ } .tl-canvas { margin: 0 6px 0 10px Index: skins/eagle/css.txt ================================================================== --- skins/eagle/css.txt +++ skins/eagle/css.txt @@ -189,10 +189,19 @@ background-color: #7EA2D9; } .timelineSecondary { background-color: #7EA27E; } +.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:white;opacity:0.2' 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; +} /* commit node */ .tl-node { width: 10px; height: 10px; Index: skins/xekri/css.txt ================================================================== --- skins/xekri/css.txt +++ skins/xekri/css.txt @@ -842,10 +842,20 @@ padding: 0 1em 0 1em; border-radius: 1rem; background: #333; box-shadow: 2px 2px 1px #000; } + +.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:white;opacity:0.2' d='M0,0h1v1h-1z'/%3E%3C/svg%3E") !important; +/*Note: IE requires explicit declarations for the next three properties.*/ + background-position: top left; + background-repeat: repeat repeat; + background-size: 64px 64px; +} .timelineTable .timelineModernCell .timelineModernComment , .timelineTable .timelineModernCell .timelineModernDetail , .timelineTable .timelineCompactCell .timelineCompactComment , .timelineTable .timelineCompactCell .timelineCompactDetail , 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:black;opacity:0.1' 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,274 @@ 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. +** ESC - 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 ESC) +** 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 kTMLN: ensure the correct page is opened when used from /finfo (it seems +** the tooltip also gets this "wrong", but maybe that's acceptable, because +** in order to be able to construct /file URLs, the information provided by +** the timeline-data-N blocks would have to be extended). +** o kFRST, kLAST: check if the previous/next page should be opened if focus is +** already at the top/bottom. +** o Tweak the focus indicator background color and opacity to be suitable for +** the light-background skins, and override it for the dark-background skins +** (Ardoise, Dark Mode, Eagle, Xekri). +** 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:not(.timelineSecondary) .tl-nodemark') + || document.querySelector('.timelineSelected .tl-nodemark') + || document.querySelector('.timelineCurrent .tl-nodemark'); + return tn ? tn.id : 'm1'; + } + function focusSelectedId(){ + var tn = document.querySelector( + '.timelineSelected:not(.timelineSecondary) .tl-nodemark'); + return tn ? tn.id : null; + } + function focus2ndSelectedId(){ + var tn = document.querySelector('.timelineSecondary .tl-nodemark'); + return tn ? tn.id : null; + } + function focusCurrentId(){ + var tn = document.querySelector('.timelineCurrent .tl-nodemark'); + return tn ? tn.id : null; + } + function focusTickedId(){ + var nd = document.querySelector('.tl-node.sel'); + return nd ? 'm' + nd.id.slice(3) : null; + } + 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 @@ -1627,10 +1627,25 @@ ** which one(s) is/are applied is unspecified and may change between ** fossil versions. ** ** CHECKIN or TIMEORTAG can be a check-in hash prefix, or a tag, or the ** name of a branch. +** +** Keyboard navigation shortcuts: +** +** N Focus first (newest) entry. +** n Focus next (newer) entry, or open next page. +** m Focus previous (older) entry, or open previous page. +** M Focus last (oldest) entry. +** h Move focus between selected, current (check-out) and ticked entries. +** , Tick/untick the node of the focused entry. +** ; Untick the nodes of all entries. +** b Copy the commit hash of the focused entry to clipboard. +** B Copy the branch name of the focused entry to clipboard. +** j View timeline of focused entry. +** k View details of focused entry. +** ESC Disable keyboard navigation mode. */ void page_timeline(void){ Stmt q; /* Query used to generate the timeline */ Blob sql; /* text of SQL used to generate timeline */ Blob desc; /* Description of the timeline */ @@ -2729,18 +2744,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(); }