Skip to content
This repository has been archived by the owner on Jul 5, 2023. It is now read-only.

In-page quick-nav menu #741

Merged
merged 8 commits into from
Apr 5, 2019
Merged

In-page quick-nav menu #741

merged 8 commits into from
Apr 5, 2019

Conversation

nfagerlund
Copy link
Member

@nfagerlund nfagerlund commented Apr 2, 2019

This adds a small drop-down menu at the top of each page. When clicked,
it reveals an in-page hierarchical TOC which you can use to jump directly to any
of the H2, H3, or H4 headers in the primary content. The TOC can be dismissed by
clicking outside it or by re-clicking the trigger.

gif:

quicknav

desktop:

Screenshot_2019-04-02 Workspaces - API Docs - Terraform Enterprise - Terraform by HashiCorp(1)

mobile:

This re-uses the SCSS from the top-nav dropdown, and uses a little bit of JQuery
to do the rest of the lifting.

@armchairlinguist
Copy link
Contributor

This is SO EXTREMELY GOOD. :terraform-da: 💜
What do you think about calling it "Page Quick Nav" for a bit more clarity?

@nfagerlund
Copy link
Member Author

"Page Quick Nav" sounds fine!

nfagerlund and others added 6 commits April 2, 2019 16:54
This commit adds a small drop-down menu at the top of each page. When clicked,
it reveals an in-page hierarchical TOC which you can use to jump directly to any
of the H2, H3, or H4 headers in the primary content. The TOC can be dismissed by
clicking outside it or by re-clicking the trigger.

This re-uses the SCSS from the top-nav dropdown, and uses a little bit of JQuery
to do the rest of the lifting. I ruled out using Ruby to get the list of headers
during the static build for a couple of reasons:

- It would be significantly harder to accomplish.
- It would be a dead-end investment, since we're moving away from the Middleman
  platform.
- It risks a nasty effect on site build times and/or HTML munging code.
  (Previous experiments at using Nogokiri to safely extract what I want from the
  content HTML have not been good, but doing it with regexps can be even worse.)

The extra event handlers to make click-based activation/deactivation work
wouldn't be necessary if I'd just used hover-based activation like in the top
navs, but that felt awkward once the TOC reached about the length of a screen.
Using margin-left was pushing the entire list item to the side,
and on mobile widths, that would sometimes cross over the edge
of the menu box. Padding doesn't move the rest of the element over,
and the links should stay inside their li.
Copy link
Contributor

@phinze phinze left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this! I'll hold off on ✅ since somebody qualified to review the code probably should give it a once-over, but I'm very +1 for it happening.

Copy link
Contributor

@jescalan jescalan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea here!


// On docs/content pages, add a hierarchical quick nav menu if there are any
// H2/H3/H4 headers.
document.addEventListener("turbolinks:load", function() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why put this in a different load function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No good reason! I'll have it move in with the other one.

// On docs/content pages, add a hierarchical quick nav menu if there are any
// H2/H3/H4 headers.
document.addEventListener("turbolinks:load", function() {
"use strict";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason why this is only in this function rather than at the top level?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I thought it was only function scope?
  • If not, would it mess with anything being pulled in by the //= require statements?

Anyway, I'll defer to you!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be global as well, but that's a fair question about messing with other statements. Honestly I don't think you have any code here that violates strict mode anyway so it probably makes little difference, but usually i see people who use it putting it right at the top of files.

);
var quickNavTrigger = $('#inner-quicknav #inner-quicknav-trigger');
var quickNav = $('#inner-quicknav > ul.dropdown');
headers.each( function(index, element) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the space before the function on purpose?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS is one of my more recently-acquired tongues, and sometimes I go overboard on trying to keep it readable for my future self. Probably not necessary.

});
$('body').on('click', function() {
quickNav.removeClass('active');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a general question - since turbolinks prevents a full reload, does every page navigation transition continue stacking more and more of these event listeners? If so, maybe should we remove the event listeners somehow - didn't see a "before route change" event here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great question. Lemme see if I can find out using the inspector tools.

I can't seem to do anything that causes doubled event listeners. After reading more, it looks like:

  • It at least doesn't cache event listeners. So they shouldn't get duplicated when someone navigates history w/ back and forward.
  • Turbolinks prevents a full reload, but it replaces the whole <body> element, right? That should cause it to discard any elements that have event listeners attached.

So, it seems like we should be good. But it's also possible I'm not seeing prod-like behavior in my preview... I'll build a complete copy of the site and test with that, as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure enough, even with a pre-built copy nothing seems to cause duplicated event listeners. The replacement of the entire <body> element and the fact that it doesn't cache event listeners for history navigation takes care of it, as expected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, thinking more about this: Turbolinks WOULD probably cache the <span> and <ul> that we add to the div, since those are new DOM elements. But since we're adding them with .html("etc"), it's actually idempotent -- .html() replaces the current contents of that div.

Which means that on back/forward navigation, we aren't messing anything up, but we ARE doing a small amount of extra work by rebuilding the quick-nav.

I could throw that into some if statements so reattach the event listeners but don't rebuild the elements. But that might be premature optimization, since it doesn't really feel bad as-is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great investigation! It seems like, so long as when the element is removed no references to it remain, we have no potential for a memory leak here. I assume that turbolinks isn't holding on to internal references in this manner, and if it did, someone would have called it out a while ago, so we're probably good here.

Copy link
Contributor

@alisdair alisdair left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of minor notes, but otherwise looks good to me!

// On docs/content pages, add a hierarchical quick nav menu if there are any
// H2/H3/H4 headers.
// Check whether turbolinks cached the quick-nav elements:
if ($("#inner #inner-quicknav ul").length == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ever non-zero, in your experience? I haven't tried it locally, but I thought turbolinks cached pages before calling turbolinks:load handlers, so I don't understand how it would ever save the generated quick-nav.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I might have misunderstood the timing on the cache! Yeah, I've never managed to make it be non-zero. I guess I might as well not test for that after all.

});
$('body').on('click', function() {
quickNav.removeClass('active');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this event handler is going to be added again and again for every page transition. You might want to namespace it, and remove it first? $('body').off('click.quicknav'); $('body').on('click.quicknav', function() { …

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha I had the same concern in my review above, but it seems as though if turbolinks replaces the entire body element and no references to the previous element remain, the event will be removed and garbage collected. It does still make me uncomfortable to see this though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think turbolinks replaces the body element, just its contents. For event handlers on all other elements I think we're safe, but I think body is a special case. I could be mistaken though!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a quick look in their docs and it says it does, although it might be worth verifying: https://github.com/turbolinks/turbolinks#navigating-with-turbolinks

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, because it's on <body> instead of a sub-element?

I tested this by having that handler just log something to the console, and navigated across a whole bunch of pages. But it just kept logging only once per click. So, I think that means it's not being re-added.

But if you think it's good practice to do the off/on thing, I can do it anyway!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol sorry, I commented without reloading the page first. Well, anyway, I verified it. 😅

Add comments, remove a single-use variable, change an ES6 template string to
concatenation.
@nfagerlund
Copy link
Member Author

Ok! Got functionality approval, got JS approval, responded to review comments (ended up removing the cache check — it seems like it'll never be a cache hit, which means it's a useless DOM query)... I'm doing this.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants