Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GTD-3 Multipage Navigation #27

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d766f66
GTD-14: A WIP proof of concept for the multipage navigation
lewisnyman May 31, 2018
f7385b9
GTD-14: Print H1 tags from frontmatter
lewisnyman Jun 4, 2018
aec7f33
GTD-14: Moved the h1s back into the markdown content They are require…
lewisnyman Jun 4, 2018
48381db
GTD-14: Modfied the table of contents generator so it prepends the pa…
lewisnyman Jun 4, 2018
8cd8ffe
GTD-14: Keep the fragment for H1 links because this still needs to wo…
lewisnyman Jun 5, 2018
4bbcd3c
GTD-14: Implemention of weight frontmatter to sort navigation items
lewisnyman Jun 5, 2018
2d62b65
GTD-14: Catch pages where weight frontmatter is not defined and sort …
lewisnyman Jun 5, 2018
c15333e
GTD-14: Do not generate the current page content twice when parsing f…
lewisnyman Jun 5, 2018
7f5a686
GTD-14: Update the in-view javascript code so it works with the new u…
lewisnyman Jun 5, 2018
415fab3
GTD-14: Proof of concept interaction for collapsible navigation
lewisnyman Jun 5, 2018
f182c0c
GTD-14: Search each top level nav item and children for an exact href…
lewisnyman Jun 5, 2018
607c1e0
GTD-14: Automatically open collapsed navigation sections if you scrol…
lewisnyman Jun 6, 2018
7418c34
GTD-14: Add some basic affordances for open/closed navigation headings
lewisnyman Jun 6, 2018
d4b9dac
GTD-14: Only iterate through the top level pages in the navigation, f…
lewisnyman Jun 7, 2018
a1bb48a
GTD-3: Improve the accessibility of the collapsible TOC navigation
lewisnyman Jun 12, 2018
f73c3b8
GTD-3: Add weight frontmatter documentation
lewisnyman Jun 12, 2018
6af07c3
GTD-3: Avoid parsing the heading structure of redirect pages
lewisnyman Jun 12, 2018
7368edb
GTD-3: Add focus styling to toc toggle button
lewisnyman Jun 12, 2018
dfb1828
GTD-3: Fixing tests
lewisnyman Jun 12, 2018
ebca17c
GTD-3 Moved multiple page tree navigation into a helper
lewisnyman Jun 14, 2018
8d725bd
GTD-3: Added a unit test for multiple page navigation
lewisnyman Jun 14, 2018
0461085
GTD-3: Ensure multipage nav still supports single page sites
lewisnyman Jun 14, 2018
cc3d4a9
GTD-3: Added an integration test for multipage navigation
lewisnyman Jun 14, 2018
0c716e7
GTD-3: Tweaked the alignment of the toggle icons
lewisnyman Jun 18, 2018
3ff59a2
GTD-3: Replaced the +- text with arrow icons based on Stephen's feedback
lewisnyman Jun 19, 2018
a9fc850
GTD-3: Fix broken text toggle for screen readers
lewisnyman Jun 19, 2018
7619da7
GTD-3: A few spacing tweaks for mobile devices
lewisnyman Jun 19, 2018
b065dd4
GTD-3: Add multipage_nav option
lewisnyman Jun 19, 2018
4507fdc
GTD-3: Moved resource selection logic into helper a fixed tests.
lewisnyman Jun 20, 2018
12186c3
GTD-3: Added support for nested page structures
lewisnyman Jun 20, 2018
01b1780
GTD-3: Ensure navigation text and collapsible toggle doesn't overlap
lewisnyman Jun 27, 2018
0f4b485
GTD-3: Automatically expand navigation items when on their child pages
lewisnyman Jun 27, 2018
ea7c538
GTD-3: Added an exception root page fpr the active trail check for co…
lewisnyman Jun 27, 2018
13e5572
GTD-3: Tweak the active navigation item code to support nested pages
lewisnyman Jun 27, 2018
9e99df6
GTD-3: Split collapsible navigation into a seperate javascript module…
lewisnyman Jun 27, 2018
e83ef00
GTD-3: Cleaned up whitespace and updated tests to match nested page code
lewisnyman Jun 28, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ Example:
host: https://docs.cloud.service.gov.uk
```

## `collapsible_nav`

Enable collapsible navigation in the sidebar. Defaults to false;

```yaml
collapsible_nav: true
```

## `multipage_nav`

Enable multipage navigation in the sidebar. Defaults to false;

```yaml
multipage_nav: true
```

## `max_toc_heading_level`

Table of contents depth – how many levels to include in the table of contents. If your ToC is too long, reduce this number and we'll only show higher-level headings.
Expand Down
11 changes: 11 additions & 0 deletions docs/frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ title: My beautiful page
---
```

## `weight`

Affects the order a page is displayed in the sidebar navigation tree. Lower
Copy link
Contributor

Choose a reason for hiding this comment

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

This confused me a bit because a higher weight in search results often means that things will end up higher in the results. What would you think of flipping the logic and naming this navigation_priority?

Big 👍 for documenting the option.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, that seems more explicit. I'll ask @jonathanglassman for his thoughts.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm happy with either. "Weight = heavier sinks to the bottom" made sense to me, but navigation priority also makes sense. I would teach it as "smaller numbers = higher up the TOC" so as long as people don't think that higher numbers mean a higher priority in the TOC, we should be ok. If I had to make a choice I would keep it as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I see your point, it would be nice to have something that just makes sense without an explanation. It's hard to come up with a scale that's intuitive.

Jon and I had a quick chat this morning, and decided to stick with weight for now and he really connected with the analogy. However, we want to push this out wider to other writers and see if they get confused by this.

Copy link
Contributor

@MatMoore MatMoore Jun 18, 2018

Choose a reason for hiding this comment

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

I haven't had time to look at the PR as a whole yet, but just wanted to "weigh in" on this discussion...

I would interpret weight in the same way as @tijmenb - as in: more weight = more important = more prominent.

I would maybe call it something like order or navigation_order.

Copy link
Member

Choose a reason for hiding this comment

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

(navigation_)priority would be good. (navigation_)order may be vague 🤔

weights float to the top. Higher weights sink to the bottom.

```yaml
---
weight: 20
---
```

## `parent`

The page that should be highlighted as ‘active’ in the navigation.
Expand Down
6 changes: 6 additions & 0 deletions example/config/tech-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ header_links:
# Tracking ID from Google Analytics (e.g. UA-XXXX-Y)
ga_tracking_id:

# Enable multipage navigation in the sidebar
multipage_nav: true

# Enable collapsible navigation in the sidebar
collapsible_nav: true

# Table of contents depth – how many levels to include in the table of contents.
# If your ToC is too long, reduce this number and we'll only show higher-level
# headings.
Expand Down
93 changes: 93 additions & 0 deletions lib/assets/javascripts/_modules/collapsible-navigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
(function($, Modules) {
'use strict';

Modules.CollapsibleNavigation = function () {

var $contentPane;
var $nav;
var $topLevelItems;
var $headings;
var $listings;

var $openLink;
var $closeLink;

this.start = function ($element) {
$contentPane = $('.app-pane__content');
$nav = $element;
$topLevelItems = $nav.find('> ul > li');
$headings = $topLevelItems.find('> a');
$listings = $topLevelItems.find('> ul');

// Attach collapsible heading functionality,on mobile and desktop
collapsibleHeadings();
openActiveHeading();
$contentPane.on('scroll', _.debounce(openActiveHeading, 100, { maxWait: 100 }));

};

function collapsibleHeadings() {
for (var i = $topLevelItems.length - 1; i >= 0; i--) {
var $topLevelItem = $($topLevelItems[i]);
var $heading = $topLevelItem.find('> a');
var $listing = $topLevelItem.find('> ul');
// Only add collapsible functionality if there are children.
if ($listing.length == 0) {
continue;
}
$topLevelItem.addClass('collapsible');
$listing.addClass('collapsible__body')
.attr('aria-expanded', 'false');
$heading.addClass('collapsible__heading')
.after('<button class="collapsible__toggle"><span class="collapsible__toggle-label">Expand ' + $heading.text() + '</span><span class="collapsible__toggle-icon" aria-hidden="true"></button>')
$topLevelItem.on('click', '.collapsible__toggle', function(e) {
e.preventDefault();
var $parent = $(this).parent();
toggleHeading($parent);
});
}
}

function toggleHeading($topLevelItem) {
var isOpen = $topLevelItem.hasClass('is-open');
var $heading = $topLevelItem.find('> a');
var $body = $topLevelItem.find('collapsible__body');
var $toggleLabel = $topLevelItem.find('.collapsible__toggle-label');

$topLevelItem.toggleClass('is-open', !isOpen);
$body.attr('aria-expanded', isOpen ? 'true' : 'false');
$toggleLabel.text(isOpen ? 'Expand ' + $heading.text() : 'Collapse ' + $heading.text());
}

function openActiveHeading() {
var $activeElement;
var currentPath = window.location.pathname;
var isActiveTrail = '[href*="' + currentPath + '"]';
// Add an exception for the root page, as every href includes /
if(currentPath == '/') {
isActiveTrail = '[href="' + currentPath + window.location.hash + '"]'
}
for (var i = $topLevelItems.length - 1; i >= 0; i--) {
var $element = $($topLevelItems[i]);
var $heading = $element.find('> a');
// Check if this item href matches
if($heading.is(isActiveTrail)) {
$activeElement = $element;
break;
}
// Otherwise check the children
var $children = $element.find('li > a');
var $matchingChildren = $children.filter(isActiveTrail);
if ($matchingChildren.length) {
$activeElement = $element;
break;
}
}
if($activeElement && !$activeElement.hasClass('is-open')) {
toggleHeading($activeElement);
}
}


};
})(jQuery, window.GOVUK.Modules);
8 changes: 6 additions & 2 deletions lib/assets/javascripts/_modules/in-page-navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@
}

function highlightActiveItemInToc(fragment) {
var $activeTocItem = $tocItems.filter('[href="' + fragment + '"]');

var $activeTocItem = $tocItems.filter('[href="' + window.location.pathname + fragment + '"]');
// Navigation items with children don't contain fragments in their url
// Check to see if any nav items contain just the path name.
if(!$activeTocItem.get(0)) {
$activeTocItem = $tocItems.filter('[href="' + window.location.pathname + '"]');
}
if ($activeTocItem.get(0)) {
$tocItems.removeClass('toc-link--in-view');
$activeTocItem.addClass('toc-link--in-view');
Expand Down
12 changes: 10 additions & 2 deletions lib/assets/javascripts/_modules/table-of-contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,23 @@
Modules.TableOfContents = function () {
var $html = $('html');

var $contentPane;
var $toc;
var $tocList;
var $topLevelItems;
var $headings;
var $listings;

var $openLink;
var $closeLink;

this.start = function ($element) {
$contentPane = $('.app-pane__content');
$toc = $element;
$tocList = $toc.find('.js-toc-list');
$topLevelItems = $tocList.find('> ul > li');
$headings = $topLevelItems.find('> a');
$listings = $topLevelItems.find('> ul');

// Open link is not inside the module
$openLink = $html.find('.js-toc-show');
Expand Down Expand Up @@ -44,7 +52,7 @@
// scrolling in that direction will scroll the body 'behind' the table of
// contents. Fix this by preventing ever reaching the top or bottom of the
// table of contents (by 1 pixel).
//
//
// http://blog.christoffer.me/six-things-i-learnt-about-ios-safaris-rubber-band-scrolling/
$toc.on("touchstart.toc", function () {
var $this = $(this),
Expand All @@ -62,7 +70,7 @@

function openNavigation() {
$html.addClass('toc-open');

toggleBackgroundVisiblity(false);
updateAriaAttributes();

Expand Down
1 change: 1 addition & 0 deletions lib/assets/javascripts/_start-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//= require _modules/navigation
//= require _modules/page-expiry
//= require _modules/table-of-contents
//= require _modules/collapsible-navigation

$(document).ready(function() {
GOVUK.modules.start();
Expand Down
1 change: 1 addition & 0 deletions lib/assets/stylesheets/_core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ $desktop-breakpoint: 992px !default;
@import "modules/contribution-banner";
@import "modules/technical-documentation";
@import "modules/toc";
@import "modules/collapsible";

@import "accessibility";

Expand Down
45 changes: 45 additions & 0 deletions lib/assets/stylesheets/modules/_collapsible.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Collapsible JS component styling, made for the navigation tree.
// These classes are added in table-of-contents.js.
// They should not be applied without the JS.

.collapsible {
position: relative;
}
.collapsible__body {
display: none;
.collapsible.is-open & {
display: block
}
}
.collapsible__toggle {
position: absolute;
top: 0;
right: -25px;
width: 50px;
height: 40px;
overflow: hidden;
text-indent: -999em;
border: 0;
background: 0;
color: inherit;
padding: 0;
&:focus {
outline: 3px solid $focus-colour;
}
}
.collapsible__toggle-icon {
position: absolute;
top: 0;
right: 30px;
&::after {
content: '';
display: block;
background: no-repeat file-url('arrow-down.svg') center center;
background-size: 18px auto;
width: 20px;
height: 40px;
}
.collapsible.is-open &::after {
background-image: file-url('arrow-up.svg');
}
}
7 changes: 1 addition & 6 deletions lib/assets/stylesheets/modules/_toc.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

a:link, a:visited {
display: block;
padding: 8px $gutter-half;
padding: 8px 40px 8px $gutter-half;
margin: 0 $gutter-half * -1;
border-left: 5px solid transparent;

Expand All @@ -46,11 +46,6 @@
}

@include media(tablet) {
// Level 2
> ul > li > ul {
margin-bottom: 20px;
}

// Level 3
li li li {
a:link, a:visited {
Expand Down
5 changes: 3 additions & 2 deletions lib/govuk_tech_docs/table_of_contents/heading.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
module GovukTechDocs
module TableOfContents
class Heading
def initialize(element_name:, text:, attributes:)
def initialize(element_name:, text:, attributes:, page_url: '')
@element_name = element_name
@text = text
@attributes = attributes
@page_url = page_url
end

def size
@element_name.scan(/h(\d)/) && $1 && Integer($1)
end

def href
'#' + @attributes['id']
@page_url + '#' + @attributes['id']
end

def title
Expand Down
6 changes: 4 additions & 2 deletions lib/govuk_tech_docs/table_of_contents/headings_builder.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
module GovukTechDocs
module TableOfContents
class HeadingsBuilder
def initialize(html)
def initialize(html, url)
@html = html
@url = url
end

def headings
heading_elements.map do |element|
Heading.new(
element_name: element.node_name,
text: element.content,
attributes: convert_nokogiri_attr_objects_to_hashes(element.attributes)
attributes: convert_nokogiri_attr_objects_to_hashes(element.attributes),
page_url: @url
)
end
end
Expand Down
53 changes: 50 additions & 3 deletions lib/govuk_tech_docs/table_of_contents/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,63 @@
module GovukTechDocs
module TableOfContents
module Helpers
def table_of_contents(html, max_level: nil)
headings = HeadingsBuilder.new(html).headings
def single_page_table_of_contents(html, url: '', max_level: nil)
headings = HeadingsBuilder.new(html, url).headings

if headings.none? { |heading| heading.size == 1 }
raise "No H1 tag found. You have to at least add one H1 heading to the page."
raise "No H1 tag found. You have to at least add one H1 heading to the page: " + url
end

tree = HeadingTreeBuilder.new(headings).tree
HeadingTreeRenderer.new(tree, max_level: max_level).html
end

def multi_page_table_of_contents(resources, current_page, config, current_page_html = nil)
# Only parse top level html files
# Sorted by weight frontmatter
resources = resources
.select { |r| r.path.end_with?(".html") && (r.parent.nil? || r.parent.url == "/") }
.sort_by { |r| [r.data.weight ? 0 : 1, r.data.weight || 0] }

render_page_tree(resources, current_page, config, current_page_html)
end

def render_page_tree(resources, current_page, config, current_page_html)
# Sort by weight frontmatter
resources = resources
.sort_by { |r| [r.data.weight ? 0 : 1, r.data.weight || 0] }
output = '';
resources.each do |resource|
# Reuse the generated content for the active page
# If we generate it twice it increments the heading ids
content =
if current_page.url == resource.url && current_page_html
current_page_html
else
resource.render(layout: false)
end
# Avoid redirect pages
next if content.include? "http-equiv=refresh"
# If this page has children, just print the title and recursively
# render the children.
# If not, print the heading structure.
# We avoid printing the children of the root index.html as it is the
# parent of every other top level file.
if resource.children.any? && resource.url != "/"
output += %{<ul><li><a href="#{resource.url}">#{resource.data.title}</a>\n}
output += render_page_tree(resource.children, current_page, config, current_page_html)
output += '</li></ul>'
else
output +=
single_page_table_of_contents(
content,
url: resource.url,
max_level: config[:tech_docs][:max_toc_heading_level]
)
end
end
output
end
end
end
end
Loading