Skip to content

Commit

Permalink
Merge pull request #27 from ConvivioTeam/GTD-3-multipage-navigation
Browse files Browse the repository at this point in the history
GTD-3 Multipage Navigation
  • Loading branch information
jonathanglassman authored Jul 3, 2018
2 parents f2e2753 + e83ef00 commit 90b6ac0
Show file tree
Hide file tree
Showing 20 changed files with 426 additions and 29 deletions.
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
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

0 comments on commit 90b6ac0

Please sign in to comment.