UNPKG

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