From d385b8c1ee93b8304c74bb5edc9c5f0964ab700b Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Mon, 11 Apr 2022 16:30:04 -0400 Subject: [PATCH] Hovering over message highlights TOC item (#598) --- CHANGELOG.md | 7 ++++ client/message.coffee | 98 +++++++++++++++++++++++++++++-------------- client/message.styl | 26 ++++++++++-- 3 files changed, 95 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0cf7e4..5ae7acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ To see every change with descriptions aimed at developers, see As a continuously updated web app, Coauthor uses dates instead of version numbers. +## 2022-04-11 + +* Hovering over a message highlights the corresponding item + of the table of contents, or if that item is out of visual range, + the relevant border. + [[#598](https://github.com/edemaine/coauthor/issues/598)] + ## 2022-03-22 * Messages can now be pinned to highlight them as "important" diff --git a/client/message.coffee b/client/message.coffee index 6038d0f..f69cb70 100644 --- a/client/message.coffee +++ b/client/message.coffee @@ -1671,6 +1671,28 @@ export TableOfContents = React.memo ({message, parent, index}) -> TableOfContents.displayName = 'TableOfContents' +tocItemTop = (item) -> + itemTop = 0 + ancestor = item.parentNode + while ancestor? and not /sticky/.test ancestor.className + itemTop += ancestor.offsetTop + ancestor = ancestor.offsetParent + itemTop + +tocHoverIndicator = _.debounce -> + toc = document.querySelector 'nav.contents' + return unless toc? # not in a view with table of contents + toc.previousSibling?.classList.remove 'active' + toc.nextSibling?.classList.remove 'active' + item = toc.querySelector '.hover' + return unless item? # no hovered item + top = tocItemTop item + if top < toc.scrollTop + toc.previousSibling?.classList.add 'active' + else if top > toc.scrollTop + toc.clientHeight + toc.nextSibling?.classList.add 'active' +, 100 + export WrappedTableOfContents = React.memo ({message, parent, index}) -> isRoot = not parent? # should not differ between calls (for hook properties) formattedTitle = useTracker -> @@ -1739,14 +1761,18 @@ export WrappedTableOfContents = React.memo ({message, parent, index}) -> if isRoot - + <> +
+ +
+ else
  • {inner} @@ -2367,36 +2393,44 @@ export WrappedSubmessage = React.memo ({message, read}) -> ## Scroll the table of contents (if visible) to align with this message (if ## possible), and pulse the table of contents item (for when not possible). showTOC = (e) -> - ## Ignore propagated events e.g. from Action dropdown button. - return unless e.target.className.startsWith 'panel-heading' e.preventDefault() + ## Find corresponding table of contents entry toc = document.querySelector 'nav.contents' - return unless toc? + return unless toc? # not in a view with table of contents item = toc.querySelector "a[data-id='#{message._id}']" - return unless item? + return unless item? # this message is hidden in table of contents msgTop = e.currentTarget.getBoundingClientRect().top - itemTop = 0 - ancestor = item.parentNode - while ancestor? and not /sticky/.test ancestor.className - itemTop += ancestor.offsetTop - ancestor = ancestor.offsetParent - toc.scroll - top: itemTop - msgTop - 9 # fudge factor to align heading with toc item - behavior: scrollBehavior() - ## Pulse item - item.animate [ - transform: 'scale(1)' - , - transform: if prefersReducedMotion() then 'scale(1)' else 'scale(0.95)' - backgroundColor: '#888' - , - transform: 'scale(1)' - ], - duration: 500 - iterations: 3 + itemTop = tocItemTop item + switch e.type + when 'mouseenter' + item.classList.add 'hover' + tocHoverIndicator() + when 'mouseleave' + item.classList.remove 'hover' + tocHoverIndicator() + when 'click' + ## Ignore propagated click events e.g. from Action dropdown button. + return unless e.target.className.startsWith 'panel-heading' + ## Scroll to align TOC item with message header + toc.scroll + top: itemTop - msgTop - 9 # fudge factor + behavior: scrollBehavior() + ## Pulse TOC item + item.animate [ + transform: 'scale(1)' + , + transform: + if prefersReducedMotion() then 'scale(1)' else 'scale(0.95)' + backgroundColor: '#888' + , + transform: 'scale(1)' + ], + duration: 500 + iterations: 3
    -
    +
    {if editing and not history? else diff --git a/client/message.styl b/client/message.styl index 57b5284..718a7af 100644 --- a/client/message.styl +++ b/client/message.styl @@ -188,6 +188,22 @@ bootstrap-3-button(bg-color) // based on mixins/buttons.less .toc-toggle margin-top: -19.39px // rendered height, putting exactly into margin +.indicator + position: absolute + left: 0 + width: calc(100% - 15px) // 15px from right padding of .col-xs-3 parent + height: 8px + border: 4px solid transparent + z-index: 2000 // above 1020 from .sticky-top + &.top + top: 0 + &.active + border-top-color: #f0f + &.bottom + bottom: 0 + &.active + border-bottom-color: #f0f + nav.contents padding-bottom: 5px height: 100% // match parent height set in JavaScript @@ -254,6 +270,12 @@ nav.contents background: #ebe2f8 /.dark & background: #241835 + // Hovering over corresponding message overrides color from visibility + // to a hue of 300 + a.hover + background: #f8bff8 !important + /.dark & + background: #5c1d5c !important .subcontents border-left: solid 2px #8465b3 @@ -292,10 +314,6 @@ nav.contents // Message heading -.message - > .panel-heading:hover - border-color: red - // Message body .message-body