UNPKG

8.33 kBJavaScriptView Raw
1'use strict'
2
3const d3 = require('./d3.js')
4const icons = require('./icons.js')
5const categories = require('./categories.js')
6const EventEmitter = require('events')
7
8class RecomendationWrapper {
9 constructor (categoryContent) {
10 this.content = categoryContent
11 this.category = this.content.category
12 this.menu = this.content.menu
13 this.title = this.content.title
14
15 this.selected = false
16 this.detected = false
17 }
18
19 get order () {
20 // always make the detected issue appear first
21 return this.detected ? 0 : this.content.order
22 }
23
24 getSummaryTitle () {
25 if (this.detected) return `Doctor has found ${this.title}:`
26
27 return `Doctor has not found evidence of ${this.title}.` +
28 ' When such issues are present:'
29 }
30
31 getSummary () { return this.content.getSummary() }
32 hasSummary () { return this.content.hasSummary() }
33
34 getReadMore () { return this.content.getReadMore() }
35 hasReadMore () { return this.content.hasReadMore() }
36}
37
38class Recomendation extends EventEmitter {
39 constructor () {
40 super()
41
42 this.readMoreOpened = false
43 this.panelOpened = false
44 this.selectedCategory = 'unknown'
45
46 // wrap content with selected and detected properties
47 this.recommendations = new Map()
48 this.recommendationsAsArray = []
49 for (const categoryContent of categories.asArray()) {
50 const wrapper = new RecomendationWrapper(categoryContent)
51 this.recommendations.set(wrapper.category, wrapper)
52 this.recommendationsAsArray.push(wrapper)
53 }
54
55 // create HTML structure
56 this.space = d3.select('#recommendation-space')
57
58 this.container = d3.select('#recommendation')
59 .classed('open', this.panelOpened)
60
61 this.details = this.container.append('div')
62 .classed('details', true)
63 this.menu = this.details.append('div')
64 .classed('menu', true)
65 this.content = this.details.append('div')
66 .classed('content', true)
67 .on('scroll.scroller', () => this._drawSelectedArticleMenu())
68 this.summaryTitle = this.content.append('div')
69 .classed('summary-title', true)
70 this.summary = this.content.append('div')
71 .classed('summary', true)
72 this.readMoreButton = this.content.append('div')
73 .classed('read-more-button', true)
74 .on('click', () => this.emit(this.readMoreOpened ? 'close-read-more' : 'open-read-more'))
75 this.readMore = this.content.append('div')
76 .classed('read-more', true)
77
78 this.articleMenu = this.readMore.append('nav')
79 .classed('article-menu', true)
80
81 this.readMoreArticle = this.readMore.append('article')
82 .classed('article', true)
83
84 this.pages = this.menu.append('ul')
85 const pagesLiEnter = this.pages
86 .selectAll('li')
87 .data(this.recommendationsAsArray, (d) => d.category)
88 .enter()
89 .append('li')
90 .classed('recommendation-tab', true)
91 .on('click', (d) => this.emit('menu-click', d.category))
92 pagesLiEnter.append('span')
93 .classed('menu-text', true)
94 .attr('data-content', (d) => d.menu)
95 pagesLiEnter.append('svg')
96 .classed('warning-icon', true)
97 .call(icons.insertIcon('warning'))
98
99 // Add button to show-hide tabs described undetected issues
100 const button = this.pages.append('li')
101 .classed('show-hide', true)
102 .on('click', () => this.emit(this.undetectedOpened ? 'close-undetected' : 'open-undetected'))
103 .append('span')
104 .classed('menu-text', true)
105
106 const buttonText = button
107 .append('span')
108 .classed('menu-text-inner', true)
109 buttonText
110 .append('svg')
111 .call(icons.insertIcon('arrow-left'))
112 buttonText
113 .append('span')
114 .text('Browse undetected issues')
115 button
116 .append('span')
117 .text('Hide')
118 .classed('menu-text-inner menu-text-inner-hide', true)
119 .append('svg')
120 .call(icons.insertIcon('arrow-right'))
121
122 const readMoreText = this.readMoreButton.append('span')
123 .classed('read-more-button-text', true)
124 .text('Read more')
125 readMoreText
126 .append('svg')
127 .call(icons.insertIcon('arrow-down'))
128
129 const readLessText = this.readMoreButton.append('span')
130 .classed('read-more-button-text read-more-button-text-less', true)
131 .text('Read less')
132 readLessText
133 .append('svg')
134 .call(icons.insertIcon('arrow-up'))
135
136 this.menu.append('svg')
137 .classed('close', true)
138 .on('click', () => this.emit('close-panel'))
139 .call(icons.insertIcon('close'))
140
141 this.bar = this.container.append('div')
142 .classed('bar', true)
143 .on('click', () => this.emit(this.panelOpened ? 'close-panel' : 'open-panel'))
144 this.bar.append('div')
145 .classed('text', true)
146 const arrow = this.bar.append('div')
147 .classed('arrow', true)
148 arrow.append('svg')
149 .classed('arrow-up', true)
150 .call(icons.insertIcon('arrow-up'))
151 arrow.append('svg')
152 .classed('arrow-down', true)
153 .call(icons.insertIcon('arrow-down'))
154 }
155
156 setData (data) {
157 this.defaultCategory = data.analysis.issueCategory
158 this.recommendations.get(this.defaultCategory).detected = true
159
160 // reorder pages, such that the detected page selector comes first
161 this.pages
162 .selectAll('li.recommendation-tab')
163 .sort((a, b) => a.order - b.order)
164
165 // set the default page
166 this.setPage(this.defaultCategory)
167 }
168
169 setPage (newCategory) {
170 const oldCategory = this.selectedCategory
171 this.selectedCategory = newCategory
172 this.recommendations.get(oldCategory).selected = false
173 this.recommendations.get(newCategory).selected = true
174 }
175
176 draw () {
177 this.pages
178 .selectAll('li.recommendation-tab')
179 .data(this.recommendationsAsArray, (d) => d.category)
180 .classed('detected', (d) => d.detected)
181 .classed('selected', (d) => d.selected)
182 .classed('has-read-more', (d) => d.hasReadMore())
183
184 const recommendation = this.recommendations.get(this.selectedCategory)
185
186 // update state classes
187 this.container
188 .classed('open', this.panelOpened)
189 .classed('read-more-open', this.readMoreOpened)
190 .classed('undetected-opened', this.undetectedOpened)
191 .classed('has-read-more', recommendation.hasReadMore())
192
193 // set content
194 this.summaryTitle.text(recommendation.getSummaryTitle())
195 this.summary.html(null)
196 if (recommendation.hasSummary()) {
197 this.summary.node().appendChild(recommendation.getSummary())
198 }
199
200 this.readMoreArticle.html(null)
201 this.articleMenu.html(null)
202 if (recommendation.hasReadMore()) {
203 this.readMoreArticle.node().appendChild(recommendation.getReadMore())
204
205 this.articleMenu.append('h2')
206 .text('Jump to section')
207
208 this.articleMenu.append('ul')
209 .selectAll('li')
210 .data(this.readMoreArticle.selectAll('h2').nodes())
211 .enter()
212 .append('li')
213 .text((headerElement) => headerElement.textContent)
214 .on('click', function (headerElement) {
215 headerElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
216 })
217
218 this._drawSelectedArticleMenu()
219 }
220
221 // set space height such that the fixed element don't have to hide
222 // something in the background.
223 this.space.style('height', this.details.node().offsetHeight + 'px')
224 }
225
226 _drawSelectedArticleMenu () {
227 const contentScrollTop = this.content.node().scrollTop
228 const contentClientHeight = this.content.node().clientHeight
229
230 function isAboveScrollBottom (headerElement) {
231 const elementBottom = headerElement.offsetTop + headerElement.clientHeight
232 const relativeTopPosition = elementBottom - contentScrollTop
233 return relativeTopPosition <= contentClientHeight
234 }
235
236 const selection = this.articleMenu.select('ul').selectAll('li')
237 const mostRecentHeader = selection.data()
238 .filter(isAboveScrollBottom)
239 .pop()
240
241 selection.classed('selected', function (headerElement) {
242 return headerElement === mostRecentHeader
243 })
244 }
245
246 openPanel () {
247 this.panelOpened = true
248 }
249 closePanel () {
250 this.panelOpened = false
251 }
252
253 openReadMore () {
254 this.readMoreOpened = true
255 }
256 closeReadMore () {
257 this.readMoreOpened = false
258 }
259
260 openUndetected () {
261 this.undetectedOpened = true
262 }
263 closeUndetected () {
264 this.undetectedOpened = false
265 }
266}
267
268module.exports = new Recomendation()