govuk_publishing_components 5.5.2 → 5.5.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbffa0f1c20e03207e8fb4483ea072b971524638ce9e8f0d484124712fe51aa8
4
- data.tar.gz: '037786464913c7c60198ab795f0c464c828afb79b96d37f07d7ca3eaf8eb266f'
3
+ metadata.gz: 04c80df6888c6c4100549e5343d78349aef6050112323f2caf4368d3baab6a05
4
+ data.tar.gz: 17ec1aed143598ea755942bcb5d1f57d45291f51c1c662b8ec39a97234ba7082
5
5
  SHA512:
6
- metadata.gz: 3f4a7adb16f3ab924319303569a2ab9c5a83e1a0f4c6d659c7f17c52dfb93bd088c3cb7bd789023c1e2e7cea3fac32dc8b5dfb2ee120ed4eda5f6581530dc357
7
- data.tar.gz: 43b7689d69c77b3b7749ba441d2590ccdcdbf37e7064f5bc7fcfd1b557bef3157eab79bf3105f2bc4d73d0faa6544c9442103eb34837c4fc7ffe177b0e48f856
6
+ metadata.gz: 72eec3cbce81a2300714b390d4321008984f151c477bb84c35e3a1a60fe47a4052554ec958e08369d68f21db0dfa29046e362be73e435988f40e380a432558d7
7
+ data.tar.gz: a091a509f7e17c818c6e8ac1b3f6edfc826c7bbe3a2b018b0ba576bc070210a6594efd794b376dde4d85c59e308ecf159ac40dbb18625d2532bde8a3cb342552
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # GOV.UK Publishing Components
2
2
 
3
- A gem to 1) document components in GOV.UK frontend applications and 2) provide shared components to applications.
3
+ This gem:
4
+
5
+ - Provides shared components for applications
6
+ - Provides helpers to generate component payloads
7
+ - Provides an application to preview components
4
8
 
5
9
  Components should be added to this gem if they are required in more than one application, otherwise they should be added to that application.
6
10
 
@@ -15,6 +19,27 @@ Components should be added to this gem if they are required in more than one app
15
19
  - [Run the component guide](/docs/run-component-guide.md)
16
20
  - [Move a component from an application to the gem](/docs/moving-components-upstream-into-this-gem.md)
17
21
 
22
+ ## Architecture / structure
23
+
24
+ ![](https://docs.google.com/drawings/d/e/2PACX-1vRj6JM7cQvngDl3Gr_U9G4xga2gsU7Z-d2qHHQcsBdjsW4WaC9_eQdryBJIS69cLkrY7S0fK9BcrPSF/pub?w=960&h=720)
25
+
26
+ [Source](https://docs.google.com/drawings/d/1N8-kbyCN_xOvvshN6d2HnQz5i5Bqed2WIatI3Nj9gNQ/edit)
27
+
28
+ There are 2 types of helper classes in this app:
29
+
30
+ - [AppHelpers](lib/govuk_publishing_components/app_helpers). Are exposed to the applications using this gem. They should be documented using RDoc.
31
+ - [Component Presenters](lib/govuk_publishing_components/presenters). Anything in these classes is only for use within the components. They should be marked `@private`.
32
+
33
+ ## Documentation
34
+
35
+ [See the rubydoc.info documentation](http://www.rubydoc.info/gems/govuk_publishing_components)
36
+
37
+ Run the documentation locally with:
38
+
39
+ ```
40
+ bundle exec yard server --reload
41
+ ```
42
+
18
43
  ## Running tests
19
44
 
20
45
  The default rake task runs all tests:
@@ -1,10 +1,15 @@
1
1
  .gem-c-inverse-header {
2
2
  width: 100%;
3
3
  background-color: $govuk-blue;
4
+ color: $white;
4
5
  margin-bottom: $gutter;
5
6
  padding: $gutter-half;
6
7
  }
7
8
 
9
+ .gem-c-inverse-header a {
10
+ color: $white;
11
+ }
12
+
8
13
  .gem-c-inverse-header--full-width {
9
14
  padding-left: 0;
10
15
  padding-right: 0;
@@ -1,4 +1,5 @@
1
1
  module GovukPublishingComponents
2
+ # @private
2
3
  class ComponentDocResolver
3
4
  def initialize(gem_components: false)
4
5
  @documentation_directory = gem_components ? gem_documentation_directory : app_documentation_directory
@@ -1,4 +1,4 @@
1
- <% related_nav_helper = RelatedNavigationHelper.new(local_assigns) %>
1
+ <% related_nav_helper = GovukPublishingComponents::Presenters::RelatedNavigationHelper.new(local_assigns) %>
2
2
  <% related_content = related_nav_helper.related_navigation %>
3
3
  <% if related_content.map(&:values).flatten.any? || related_nav_helper.other.flatten.any? %>
4
4
  <% random = SecureRandom.hex(4) %>
@@ -13,6 +13,8 @@
13
13
 
14
14
  step_count = 0
15
15
  step_number = 0
16
+
17
+ step_nav_helper = GovukPublishingComponents::Presenters::StepByStepNavHelper.new
16
18
  %>
17
19
  <% if steps %>
18
20
  <div
@@ -26,7 +28,7 @@
26
28
  <%
27
29
  step_is_active = step_index + 1 == highlight_step
28
30
  step_count += 1
29
- id = StepNavHelper.new.generate_step_nav_id(step[:title])
31
+ id = step_nav_helper.generate_step_nav_id(step[:title])
30
32
 
31
33
  logic = false
32
34
  logic = step[:logic] if ["and", "or"].include? step[:logic]
@@ -71,7 +73,7 @@
71
73
  options[:link_index] = 0
72
74
  %>
73
75
  <% step[:contents].each do |element| %>
74
- <%= StepNavHelper.new.render_step_nav_element(element, options) %>
76
+ <%= step_nav_helper.render_step_nav_element(element, options) %>
75
77
  <%
76
78
  if element[:type] == 'list'
77
79
  options[:link_index] += element[:contents].length
@@ -1,7 +1,7 @@
1
1
  name: Inverse header
2
2
  description: A wrapper to contain header content in white text
3
3
  body: |
4
- This component can be passed a block of template code and will wrap it in a blue header. This is as light-touch as possible and doesn't attempt to deal with the margins and paddings of its content, neither does it enforce the white text it requires. Implemented to accomodate topic and list page headings and breadcrumbs but unopinionated about its contents.
4
+ This component can be passed a block of template code and will wrap it in a blue header. This is as light-touch as possible and doesn't attempt to deal with the margins and paddings of its content. White text is enforced on content and would need to be overriden where unwanted. Implemented to accomodate topic and list page headings and breadcrumbs but unopinionated about its contents.
5
5
 
6
6
  accessibility_criteria: |
7
7
  The component must:
@@ -1,10 +1,10 @@
1
1
  require "govuk_publishing_components/config"
2
2
  require "govuk_publishing_components/engine"
3
- require "govuk_publishing_components/components/step_by_step_nav_helper"
4
- require "govuk_publishing_components/components/related_navigation_helper"
3
+ require "govuk_publishing_components/presenters/step_by_step_nav_helper"
4
+ require "govuk_publishing_components/presenters/related_navigation_helper"
5
5
 
6
- require "govuk_publishing_components/step_nav"
7
- require "govuk_publishing_components/step_nav_helper"
6
+ require "govuk_publishing_components/app_helpers/step_nav"
7
+ require "govuk_publishing_components/app_helpers/step_nav_helper"
8
8
 
9
9
  module GovukPublishingComponents
10
10
  end
@@ -0,0 +1,29 @@
1
+ module GovukPublishingComponents
2
+ module AppHelpers
3
+ class StepNav
4
+ def initialize(content_item)
5
+ @content_item = content_item.deep_symbolize_keys
6
+ end
7
+
8
+ def title
9
+ content_item[:title]
10
+ end
11
+
12
+ def base_path
13
+ content_item[:base_path]
14
+ end
15
+
16
+ def content
17
+ content_item.dig(:details, :step_by_step_nav)
18
+ end
19
+
20
+ def steps
21
+ content_item.dig(:details, :step_by_step_nav, :steps)
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :content_item
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,91 @@
1
+ module GovukPublishingComponents
2
+ module AppHelpers
3
+ class StepNavHelper
4
+ def initialize(content_store_response, current_path)
5
+ @content_item = content_store_response.to_h
6
+ @current_path = current_path
7
+ end
8
+
9
+ def step_navs
10
+ @step_navs ||= parsed_step_navs.map do |step_nav|
11
+ StepNav.new(step_nav)
12
+ end
13
+ end
14
+
15
+ def show_sidebar?
16
+ show_header? && first_step_nav.steps.present?
17
+ end
18
+
19
+ def show_header?
20
+ step_navs.count == 1
21
+ end
22
+
23
+ def show_related_links?
24
+ step_navs.any? && step_navs.count < 5
25
+ end
26
+
27
+ def related_links
28
+ step_navs.map do |step_nav|
29
+ {
30
+ href: step_nav.base_path,
31
+ text: step_nav.title
32
+ }
33
+ end
34
+ end
35
+
36
+ def sidebar
37
+ if show_sidebar?
38
+ @sidebar ||= first_step_nav.content.tap do |sb|
39
+ configure_for_sidebar(sb)
40
+ sb.merge!(small: true, heading_level: 3)
41
+ end
42
+ end
43
+ end
44
+
45
+ def header
46
+ if show_header?
47
+ {
48
+ title: first_step_nav.title,
49
+ path: first_step_nav.base_path
50
+ }
51
+ else
52
+ {}
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :content_item, :current_path
59
+
60
+ def first_step_nav
61
+ step_navs.first
62
+ end
63
+
64
+ def steps
65
+ @steps ||= step_nav[:steps]
66
+ end
67
+
68
+ def parsed_step_navs
69
+ content_item.dig("links", "part_of_step_navs").to_a
70
+ end
71
+
72
+ def configure_for_sidebar(step_nav_content)
73
+ step_nav_content[:steps].each_with_index do |step, step_index|
74
+ step[:contents].each do |content|
75
+ next unless content[:contents]
76
+
77
+ content[:contents].each do |link|
78
+ if link[:href] == current_path
79
+ link[:active] = true
80
+ step_nav_content[:show_step] = step_index + 1
81
+ step_nav_content[:highlight_step] = step_index + 1
82
+ return step_nav_content
83
+ end
84
+ end
85
+ end
86
+ end
87
+ step_nav_content
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,247 @@
1
+ module GovukPublishingComponents
2
+ module Presenters
3
+ # @private
4
+ # Only used by the related_navigation component
5
+ class RelatedNavigationHelper
6
+ MAX_SECTION_LENGTH = 5
7
+ DEFINED_SECTIONS = %w(
8
+ related_guides
9
+ topics
10
+ collections
11
+ policies
12
+ topical_events
13
+ world_locations
14
+ statistical_data_sets
15
+ ).freeze
16
+
17
+ def initialize(content_item)
18
+ @content_item = content_item
19
+ end
20
+
21
+ def related_navigation
22
+ @related_content ||= [
23
+ { "related_items" => related_items },
24
+ { "related_guides" => related_guides },
25
+ { "collections" => related_collections },
26
+ { "topics" => related_topics },
27
+ { "policies" => related_policies },
28
+ { "topical_events" => related_topical_events },
29
+ { "world_locations" => related_world_locations },
30
+ { "statistical_data_sets" => related_statistical_data_sets },
31
+ ]
32
+
33
+ other = [related_external_links, related_contacts] || []
34
+ other.each do |sections|
35
+ sections.each do |section|
36
+ @related_content.push(
37
+ section["title"].tr(' ', '_') => section["links"]
38
+ )
39
+ end
40
+ end
41
+
42
+ @related_content
43
+ end
44
+
45
+ def other
46
+ @other ||= []
47
+ end
48
+
49
+ def construct_section_heading(section_title)
50
+ unless section_title === "related_items"
51
+ I18n.t('components.related_navigation.' + section_title, default: section_title.tr('_', ' '))
52
+ end
53
+ end
54
+
55
+ def section_css_class(css_class, section_title, link = {})
56
+ unless DEFINED_SECTIONS.include?(section_title) || link.fetch(:finder, false)
57
+ css_class += "--other"
58
+ end
59
+ css_class
60
+ end
61
+
62
+ def calculate_section_link_limit(links)
63
+ links.length == MAX_SECTION_LENGTH + 1 ? MAX_SECTION_LENGTH + 1 : MAX_SECTION_LENGTH
64
+ end
65
+
66
+ def remaining_link_count(links)
67
+ links.length - MAX_SECTION_LENGTH
68
+ end
69
+
70
+ private
71
+
72
+ def build_links_for_sidebar(collection, path_key = "base_path", additional_attr = {})
73
+ collection.map do |link|
74
+ {
75
+ path: link[path_key],
76
+ text: link["title"]
77
+ }.merge(additional_attr)
78
+ end
79
+ end
80
+
81
+ def world_location_base_path(title)
82
+ "/world/#{parameterise(title)}/news"
83
+ end
84
+
85
+ def related_items
86
+ links = build_links_for_sidebar(quick_links, "url")
87
+ mainstream_links = related_mainstream_content
88
+ related_ordered_items = link_group("ordered_related_items")
89
+ if links.any?
90
+ links + mainstream_links
91
+ else
92
+ build_links_for_sidebar(related_ordered_items) + mainstream_links
93
+ end
94
+ end
95
+
96
+ def quick_links
97
+ @content_item.dig("details", "quick_links").to_a
98
+ end
99
+
100
+ def related_world_locations
101
+ locations = link_group("world_locations")
102
+ locations.map! { |link| link.merge("base_path" => world_location_base_path(link["title"])) }
103
+ build_links_for_sidebar(locations)
104
+ end
105
+
106
+ def related_collections
107
+ collections = filter_link_type("document_collections", "document_collection")
108
+ build_links_for_sidebar(collections)
109
+ end
110
+
111
+ def filter_link_type(group, type)
112
+ links = link_group(group)
113
+ links.select do |link|
114
+ link["document_type"] == type
115
+ end
116
+ end
117
+
118
+ def related_policies
119
+ policies = filter_link_type("related_policies", "policy")
120
+ build_links_for_sidebar(policies)
121
+ end
122
+
123
+ def related_statistical_data_sets
124
+ statistical_data_sets = filter_link_type("related_statistical_data_sets", "statistical_data_set")
125
+ build_links_for_sidebar(statistical_data_sets)
126
+ end
127
+
128
+ def related_topics
129
+ topics = filter_link_type("topics", "topic")
130
+ links = build_links_for_sidebar(topics)
131
+ links << related_mainstream_topic << related_mainstream_parent_topic
132
+ deduplicate_topics_by_title(links.compact)
133
+ end
134
+
135
+ def related_topical_events
136
+ topical_events = filter_link_type("topical_events", "topical_event")
137
+ build_links_for_sidebar(topical_events)
138
+ end
139
+
140
+ def related_contacts
141
+ contacts = filter_link_type("related", "contact")
142
+ return [] unless contacts.any?
143
+ [
144
+ title: "Other contacts",
145
+ links: build_links_for_sidebar(contacts).map
146
+ ]
147
+ end
148
+
149
+ def related_external_links
150
+ external_links = @content_item.dig("details", "external_related_links").to_a
151
+ return [] unless external_links.any?
152
+ [
153
+ "title" => "Elsewhere on the web",
154
+ "links" => build_links_for_sidebar(external_links, "url", rel: 'external')
155
+ ]
156
+ end
157
+
158
+ def related_mainstream_topic
159
+ return unless tagged_to_same_mainstream_browse_page.any?
160
+ { text: parent["title"], path: parent["base_path"] }
161
+ end
162
+
163
+ def related_mainstream_parent_topic
164
+ return unless parents_tagged_to_same_mainstream_browse_page.any?
165
+ { text: grandparent["title"], path: grandparent["base_path"] }
166
+ end
167
+
168
+ def parent
169
+ link_group("parent").first
170
+ end
171
+
172
+ def grandparent
173
+ parent.dig("links", "parent", 0)
174
+ end
175
+
176
+ # This method post-processes the topics collated by the helper.
177
+ # We add mainstream browse page links if they are present, however
178
+ # if these have the same title as an existing topic we should prefer
179
+ # the mainstream version and remove the existing topic.
180
+ # @see spec/related_navigation_helper_spec.rb for test coverage.
181
+ def deduplicate_topics_by_title(topics)
182
+ is_dupe = lambda { |a, b| a && a != b && a[:text] == b[:text] }
183
+
184
+ topics.delete_if do |t|
185
+ is_dupe.call(related_mainstream_topic, t) ||
186
+ is_dupe.call(related_mainstream_parent_topic, t)
187
+ end
188
+
189
+ topics
190
+ end
191
+
192
+ def parameterise(str, sep = "-")
193
+ parameterised_str = str.gsub(/[^\w\-]+/, sep)
194
+ unless sep.nil? || sep.empty?
195
+ re_sep = Regexp.escape(sep)
196
+ # No more than one of the separator in a row.
197
+ parameterised_str.gsub!(/#{re_sep}{2,}/, sep)
198
+ # Remove leading/trailing separator.
199
+ parameterised_str.gsub!(/^#{re_sep}|#{re_sep}$/, '')
200
+ end
201
+ parameterised_str.downcase
202
+ end
203
+
204
+ def tagged_to_same_mainstream_browse_page
205
+ return [] unless parent
206
+ @tagged_to_same_mainstream_browse_page ||= related_links.select do |related_item|
207
+ links = related_item.dig("links", "mainstream_browse_pages") || []
208
+ content_ids = links.any? ? links.map { |page| page["content_id"] } : []
209
+ content_ids.include?(parent["content_id"])
210
+ end
211
+ end
212
+
213
+ def parents_tagged_to_same_mainstream_browse_page
214
+ return [] unless parent && grandparent
215
+ common_parent_content_ids = tagged_to_same_mainstream_browse_page.map { |item| item["content_id"] }
216
+
217
+ @parents_tagged_to_same_mainstream_browse_page ||= related_links.select do |related_item|
218
+ next if common_parent_content_ids.include?(related_item["content_id"])
219
+ mainstream_browse_pages = related_item.dig("links", "mainstream_browse_pages") || []
220
+ parents = mainstream_browse_pages.map { |page| page["links"]["parent"][0] }
221
+ content_ids = parents.map { |parent| parent["content_id"] }
222
+ content_ids.include?(grandparent["content_id"])
223
+ end
224
+ end
225
+
226
+ def related_links
227
+ link_group("ordered_related_items")
228
+ end
229
+
230
+ def related_mainstream_content
231
+ return [] unless @content_item["document_type"] == "detailed_guide"
232
+ content = link_group("related_mainstream_content")
233
+ build_links_for_sidebar(content)
234
+ end
235
+
236
+ def related_guides
237
+ return [] unless @content_item["document_type"] == "detailed_guide"
238
+ guides = link_group("related_guides")
239
+ build_links_for_sidebar(guides)
240
+ end
241
+
242
+ def link_group(type)
243
+ @content_item.dig("links", type).to_a
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,113 @@
1
+ module GovukPublishingComponents
2
+ module Presenters
3
+ # @private
4
+ # Only used by the step by step component
5
+ class StepByStepNavHelper
6
+ include ActionView::Helpers
7
+ include ActionView::Context
8
+
9
+ def render_step_nav_element(element, options)
10
+ @options = options
11
+ @link_index = options[:link_index]
12
+
13
+ case element[:type]
14
+ when "paragraph"
15
+ paragraph(element[:text])
16
+ when "heading"
17
+ heading(element[:text])
18
+ when "list"
19
+ list(element)
20
+ end
21
+ end
22
+
23
+ # id should be lowercase, contain only numbers and letters and replace spaces with dashes
24
+ def generate_step_nav_id(step_title)
25
+ step_title.downcase.tr(" ", "-").gsub(/[^a-z0-9\-\s]/i, '')
26
+ end
27
+
28
+ private
29
+
30
+ def paragraph(text)
31
+ content_tag(
32
+ :p,
33
+ text,
34
+ class: "gem-c-step-nav__paragraph"
35
+ )
36
+ end
37
+
38
+ def heading(text)
39
+ content_tag(
40
+ "h#{@options[:heading_level] + 1}",
41
+ text,
42
+ class: "gem-c-step-nav__heading"
43
+ )
44
+ end
45
+
46
+ def list(element)
47
+ content_tag(
48
+ get_list_element(element[:style]),
49
+ class: "gem-c-step-nav__links #{get_list_style(element[:style])}",
50
+ data: {
51
+ length: element[:contents].length
52
+ }
53
+ ) do
54
+ element[:contents].collect { |contents|
55
+ concat(
56
+ content_tag(
57
+ :li,
58
+ class: "gem-c-step-nav__link js-list-item #{link_active(contents[:active])}"
59
+ ) do
60
+ create_list_item_content(contents)
61
+ end
62
+ )
63
+ }
64
+ end
65
+ end
66
+
67
+ def create_list_item_content(link)
68
+ if link[:href]
69
+ @link_index += 1
70
+ href = link_href(link[:active], link[:href])
71
+ text = "#{link_text(link[:active], link[:text])} #{create_context(link[:context])}".html_safe
72
+
73
+ link_to(
74
+ href,
75
+ rel: ("external" if href.start_with?('http')),
76
+ data: {
77
+ position: "#{@options[:step_index] + 1}.#{@link_index}"
78
+ },
79
+ class: "gem-c-step-nav__link-item js-link"
80
+ ) do
81
+ text
82
+ end
83
+ else
84
+ link[:text]
85
+ end
86
+ end
87
+
88
+ def create_context(context)
89
+ content_tag(:span, context, class: "gem-c-step-nav__context") if context
90
+ end
91
+
92
+ def get_list_style(style)
93
+ "gem-c-step-nav__links--choice" if style == "choice"
94
+ end
95
+
96
+ def get_list_element(style)
97
+ style == "choice" ? "ul" : "ol"
98
+ end
99
+
100
+ def link_href(active, href)
101
+ active ? "#content" : href
102
+ end
103
+
104
+ def link_text(active, text)
105
+ active ? content_tag(:span, "You are currently viewing: ", class: "visuallyhidden") + text : text
106
+ end
107
+
108
+ def link_active(active)
109
+ "gem-c-step-nav__link--active" if active
110
+ end
111
+ end
112
+ end
113
+ end
@@ -1,3 +1,3 @@
1
1
  module GovukPublishingComponents
2
- VERSION = '5.5.2'.freeze
2
+ VERSION = '5.5.3'.freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: govuk_publishing_components
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.5.2
4
+ version: 5.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - GOV.UK Dev
@@ -262,6 +262,20 @@ dependencies:
262
262
  - - ">="
263
263
  - !ruby/object:Gem::Version
264
264
  version: '0'
265
+ - !ruby/object:Gem::Dependency
266
+ name: yard
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - ">="
270
+ - !ruby/object:Gem::Version
271
+ version: '0'
272
+ type: :development
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - ">="
277
+ - !ruby/object:Gem::Version
278
+ version: '0'
265
279
  description: A gem to document components in GOV.UK frontend applications
266
280
  email:
267
281
  - govuk-dev@digital.cabinet-office.gov.uk
@@ -356,13 +370,13 @@ files:
356
370
  - lib/generators/govuk_publishing_components/templates/_component.scss
357
371
  - lib/generators/govuk_publishing_components/templates/component.yml.erb
358
372
  - lib/govuk_publishing_components.rb
359
- - lib/govuk_publishing_components/components/related_navigation_helper.rb
360
- - lib/govuk_publishing_components/components/step_by_step_nav_helper.rb
373
+ - lib/govuk_publishing_components/app_helpers/step_nav.rb
374
+ - lib/govuk_publishing_components/app_helpers/step_nav_helper.rb
361
375
  - lib/govuk_publishing_components/config.rb
362
376
  - lib/govuk_publishing_components/engine.rb
363
377
  - lib/govuk_publishing_components/minitest/component_guide_test.rb
364
- - lib/govuk_publishing_components/step_nav.rb
365
- - lib/govuk_publishing_components/step_nav_helper.rb
378
+ - lib/govuk_publishing_components/presenters/related_navigation_helper.rb
379
+ - lib/govuk_publishing_components/presenters/step_by_step_nav_helper.rb
366
380
  - lib/govuk_publishing_components/version.rb
367
381
  - lib/tasks/govuk_publishing_components_tasks.rake
368
382
  homepage: https://github.com/alphagov/govuk_publishing_components
@@ -1,241 +0,0 @@
1
- class RelatedNavigationHelper
2
- MAX_SECTION_LENGTH = 5
3
- DEFINED_SECTIONS = %w(
4
- related_guides
5
- topics
6
- collections
7
- policies
8
- topical_events
9
- world_locations
10
- statistical_data_sets
11
- ).freeze
12
-
13
- def initialize(content_item)
14
- @content_item = content_item
15
- end
16
-
17
- def related_navigation
18
- @related_content ||= [
19
- { "related_items" => related_items },
20
- { "related_guides" => related_guides },
21
- { "collections" => related_collections },
22
- { "topics" => related_topics },
23
- { "policies" => related_policies },
24
- { "topical_events" => related_topical_events },
25
- { "world_locations" => related_world_locations },
26
- { "statistical_data_sets" => related_statistical_data_sets },
27
- ]
28
-
29
- other = [related_external_links, related_contacts] || []
30
- other.each do |sections|
31
- sections.each do |section|
32
- @related_content.push(
33
- section["title"].tr(' ', '_') => section["links"]
34
- )
35
- end
36
- end
37
-
38
- @related_content
39
- end
40
-
41
- def other
42
- @other ||= []
43
- end
44
-
45
- def construct_section_heading(section_title)
46
- unless section_title === "related_items"
47
- I18n.t('components.related_navigation.' + section_title, default: section_title.tr('_', ' '))
48
- end
49
- end
50
-
51
- def section_css_class(css_class, section_title, link = {})
52
- unless DEFINED_SECTIONS.include?(section_title) || link.fetch(:finder, false)
53
- css_class += "--other"
54
- end
55
- css_class
56
- end
57
-
58
- def calculate_section_link_limit(links)
59
- links.length == MAX_SECTION_LENGTH + 1 ? MAX_SECTION_LENGTH + 1 : MAX_SECTION_LENGTH
60
- end
61
-
62
- def remaining_link_count(links)
63
- links.length - MAX_SECTION_LENGTH
64
- end
65
-
66
- private
67
-
68
- def build_links_for_sidebar(collection, path_key = "base_path", additional_attr = {})
69
- collection.map do |link|
70
- {
71
- path: link[path_key],
72
- text: link["title"]
73
- }.merge(additional_attr)
74
- end
75
- end
76
-
77
- def world_location_base_path(title)
78
- "/world/#{parameterise(title)}/news"
79
- end
80
-
81
- def related_items
82
- links = build_links_for_sidebar(quick_links, "url")
83
- mainstream_links = related_mainstream_content
84
- related_ordered_items = link_group("ordered_related_items")
85
- if links.any?
86
- links + mainstream_links
87
- else
88
- build_links_for_sidebar(related_ordered_items) + mainstream_links
89
- end
90
- end
91
-
92
- def quick_links
93
- @content_item.dig("details", "quick_links").to_a
94
- end
95
-
96
- def related_world_locations
97
- locations = link_group("world_locations")
98
- locations.map! { |link| link.merge("base_path" => world_location_base_path(link["title"])) }
99
- build_links_for_sidebar(locations)
100
- end
101
-
102
- def related_collections
103
- collections = filter_link_type("document_collections", "document_collection")
104
- build_links_for_sidebar(collections)
105
- end
106
-
107
- def filter_link_type(group, type)
108
- links = link_group(group)
109
- links.select do |link|
110
- link["document_type"] == type
111
- end
112
- end
113
-
114
- def related_policies
115
- policies = filter_link_type("related_policies", "policy")
116
- build_links_for_sidebar(policies)
117
- end
118
-
119
- def related_statistical_data_sets
120
- statistical_data_sets = filter_link_type("related_statistical_data_sets", "statistical_data_set")
121
- build_links_for_sidebar(statistical_data_sets)
122
- end
123
-
124
- def related_topics
125
- topics = filter_link_type("topics", "topic")
126
- links = build_links_for_sidebar(topics)
127
- links << related_mainstream_topic << related_mainstream_parent_topic
128
- deduplicate_topics_by_title(links.compact)
129
- end
130
-
131
- def related_topical_events
132
- topical_events = filter_link_type("topical_events", "topical_event")
133
- build_links_for_sidebar(topical_events)
134
- end
135
-
136
- def related_contacts
137
- contacts = filter_link_type("related", "contact")
138
- return [] unless contacts.any?
139
- [
140
- title: "Other contacts",
141
- links: build_links_for_sidebar(contacts).map
142
- ]
143
- end
144
-
145
- def related_external_links
146
- external_links = @content_item.dig("details", "external_related_links").to_a
147
- return [] unless external_links.any?
148
- [
149
- "title" => "Elsewhere on the web",
150
- "links" => build_links_for_sidebar(external_links, "url", rel: 'external')
151
- ]
152
- end
153
-
154
- def related_mainstream_topic
155
- return unless tagged_to_same_mainstream_browse_page.any?
156
- { text: parent["title"], path: parent["base_path"] }
157
- end
158
-
159
- def related_mainstream_parent_topic
160
- return unless parents_tagged_to_same_mainstream_browse_page.any?
161
- { text: grandparent["title"], path: grandparent["base_path"] }
162
- end
163
-
164
- def parent
165
- link_group("parent").first
166
- end
167
-
168
- def grandparent
169
- parent.dig("links", "parent", 0)
170
- end
171
-
172
- # This method post-processes the topics collated by the helper.
173
- # We add mainstream browse page links if they are present, however
174
- # if these have the same title as an existing topic we should prefer
175
- # the mainstream version and remove the existing topic.
176
- # @see spec/related_navigation_helper_spec.rb for test coverage.
177
- def deduplicate_topics_by_title(topics)
178
- is_dupe = lambda { |a, b| a && a != b && a[:text] == b[:text] }
179
-
180
- topics.delete_if do |t|
181
- is_dupe.call(related_mainstream_topic, t) ||
182
- is_dupe.call(related_mainstream_parent_topic, t)
183
- end
184
-
185
- topics
186
- end
187
-
188
- def parameterise(str, sep = "-")
189
- parameterised_str = str.gsub(/[^\w\-]+/, sep)
190
- unless sep.nil? || sep.empty?
191
- re_sep = Regexp.escape(sep)
192
- # No more than one of the separator in a row.
193
- parameterised_str.gsub!(/#{re_sep}{2,}/, sep)
194
- # Remove leading/trailing separator.
195
- parameterised_str.gsub!(/^#{re_sep}|#{re_sep}$/, '')
196
- end
197
- parameterised_str.downcase
198
- end
199
-
200
- def tagged_to_same_mainstream_browse_page
201
- return [] unless parent
202
- @tagged_to_same_mainstream_browse_page ||= related_links.select do |related_item|
203
- links = related_item.dig("links", "mainstream_browse_pages") || []
204
- content_ids = links.any? ? links.map { |page| page["content_id"] } : []
205
- content_ids.include?(parent["content_id"])
206
- end
207
- end
208
-
209
- def parents_tagged_to_same_mainstream_browse_page
210
- return [] unless parent && grandparent
211
- common_parent_content_ids = tagged_to_same_mainstream_browse_page.map { |item| item["content_id"] }
212
-
213
- @parents_tagged_to_same_mainstream_browse_page ||= related_links.select do |related_item|
214
- next if common_parent_content_ids.include?(related_item["content_id"])
215
- mainstream_browse_pages = related_item.dig("links", "mainstream_browse_pages") || []
216
- parents = mainstream_browse_pages.map { |page| page["links"]["parent"][0] }
217
- content_ids = parents.map { |parent| parent["content_id"] }
218
- content_ids.include?(grandparent["content_id"])
219
- end
220
- end
221
-
222
- def related_links
223
- link_group("ordered_related_items")
224
- end
225
-
226
- def related_mainstream_content
227
- return [] unless @content_item["document_type"] == "detailed_guide"
228
- content = link_group("related_mainstream_content")
229
- build_links_for_sidebar(content)
230
- end
231
-
232
- def related_guides
233
- return [] unless @content_item["document_type"] == "detailed_guide"
234
- guides = link_group("related_guides")
235
- build_links_for_sidebar(guides)
236
- end
237
-
238
- def link_group(type)
239
- @content_item.dig("links", type).to_a
240
- end
241
- end
@@ -1,107 +0,0 @@
1
- class StepNavHelper
2
- include ActionView::Helpers
3
- include ActionView::Context
4
-
5
- def render_step_nav_element(element, options)
6
- @options = options
7
- @link_index = options[:link_index]
8
-
9
- case element[:type]
10
- when "paragraph"
11
- paragraph(element[:text])
12
- when "heading"
13
- heading(element[:text])
14
- when "list"
15
- list(element)
16
- end
17
- end
18
-
19
- # id should be lowercase, contain only numbers and letters and replace spaces with dashes
20
- def generate_step_nav_id(step_title)
21
- step_title.downcase.tr(" ", "-").gsub(/[^a-z0-9\-\s]/i, '')
22
- end
23
-
24
- private
25
-
26
- def paragraph(text)
27
- content_tag(
28
- :p,
29
- text,
30
- class: "gem-c-step-nav__paragraph"
31
- )
32
- end
33
-
34
- def heading(text)
35
- content_tag(
36
- "h#{@options[:heading_level] + 1}",
37
- text,
38
- class: "gem-c-step-nav__heading"
39
- )
40
- end
41
-
42
- def list(element)
43
- content_tag(
44
- get_list_element(element[:style]),
45
- class: "gem-c-step-nav__links #{get_list_style(element[:style])}",
46
- data: {
47
- length: element[:contents].length
48
- }
49
- ) do
50
- element[:contents].collect { |contents|
51
- concat(
52
- content_tag(
53
- :li,
54
- class: "gem-c-step-nav__link js-list-item #{link_active(contents[:active])}"
55
- ) do
56
- create_list_item_content(contents)
57
- end
58
- )
59
- }
60
- end
61
- end
62
-
63
- def create_list_item_content(link)
64
- if link[:href]
65
- @link_index += 1
66
- href = link_href(link[:active], link[:href])
67
- text = "#{link_text(link[:active], link[:text])} #{create_context(link[:context])}".html_safe
68
-
69
- link_to(
70
- href,
71
- rel: ("external" if href.start_with?('http')),
72
- data: {
73
- position: "#{@options[:step_index] + 1}.#{@link_index}"
74
- },
75
- class: "gem-c-step-nav__link-item js-link"
76
- ) do
77
- text
78
- end
79
- else
80
- link[:text]
81
- end
82
- end
83
-
84
- def create_context(context)
85
- content_tag(:span, context, class: "gem-c-step-nav__context") if context
86
- end
87
-
88
- def get_list_style(style)
89
- "gem-c-step-nav__links--choice" if style == "choice"
90
- end
91
-
92
- def get_list_element(style)
93
- style == "choice" ? "ul" : "ol"
94
- end
95
-
96
- def link_href(active, href)
97
- active ? "#content" : href
98
- end
99
-
100
- def link_text(active, text)
101
- active ? content_tag(:span, "You are currently viewing: ", class: "visuallyhidden") + text : text
102
- end
103
-
104
- def link_active(active)
105
- "gem-c-step-nav__link--active" if active
106
- end
107
- end
@@ -1,27 +0,0 @@
1
- module GovukPublishingComponents
2
- class StepNav
3
- def initialize(content_item)
4
- @content_item = content_item.deep_symbolize_keys
5
- end
6
-
7
- def title
8
- content_item[:title]
9
- end
10
-
11
- def base_path
12
- content_item[:base_path]
13
- end
14
-
15
- def content
16
- content_item.dig(:details, :step_by_step_nav)
17
- end
18
-
19
- def steps
20
- content_item.dig(:details, :step_by_step_nav, :steps)
21
- end
22
-
23
- private
24
-
25
- attr_reader :content_item
26
- end
27
- end
@@ -1,89 +0,0 @@
1
- module GovukPublishingComponents
2
- class StepNavHelper
3
- def initialize(content_store_response, current_path)
4
- @content_item = content_store_response.to_h
5
- @current_path = current_path
6
- end
7
-
8
- def step_navs
9
- @step_navs ||= parsed_step_navs.map do |step_nav|
10
- StepNav.new(step_nav)
11
- end
12
- end
13
-
14
- def show_sidebar?
15
- show_header? && first_step_nav.steps.present?
16
- end
17
-
18
- def show_header?
19
- step_navs.count == 1
20
- end
21
-
22
- def show_related_links?
23
- step_navs.any? && step_navs.count < 5
24
- end
25
-
26
- def related_links
27
- step_navs.map do |step_nav|
28
- {
29
- href: step_nav.base_path,
30
- text: step_nav.title
31
- }
32
- end
33
- end
34
-
35
- def sidebar
36
- if show_sidebar?
37
- @sidebar ||= first_step_nav.content.tap do |sb|
38
- configure_for_sidebar(sb)
39
- sb.merge!(small: true, heading_level: 3)
40
- end
41
- end
42
- end
43
-
44
- def header
45
- if show_header?
46
- {
47
- title: first_step_nav.title,
48
- path: first_step_nav.base_path
49
- }
50
- else
51
- {}
52
- end
53
- end
54
-
55
- private
56
-
57
- attr_reader :content_item, :current_path
58
-
59
- def first_step_nav
60
- step_navs.first
61
- end
62
-
63
- def steps
64
- @steps ||= step_nav[:steps]
65
- end
66
-
67
- def parsed_step_navs
68
- content_item.dig("links", "part_of_step_navs").to_a
69
- end
70
-
71
- def configure_for_sidebar(step_nav_content)
72
- step_nav_content[:steps].each_with_index do |step, step_index|
73
- step[:contents].each do |content|
74
- next unless content[:contents]
75
-
76
- content[:contents].each do |link|
77
- if link[:href] == current_path
78
- link[:active] = true
79
- step_nav_content[:show_step] = step_index + 1
80
- step_nav_content[:highlight_step] = step_index + 1
81
- return step_nav_content
82
- end
83
- end
84
- end
85
- end
86
- step_nav_content
87
- end
88
- end
89
- end