UNPKG

@nearform/doctor

Version:
239 lines (197 loc) 7.43 kB
'use strict' const d3 = require('./d3.js') const icons = require('./icons.js') const categories = require('./categories.js') const EventEmitter = require('events') class RecomendationWrapper { constructor (categoryContent) { this.content = categoryContent this.category = this.content.category this.menu = this.content.menu this.title = this.content.title this.selected = false this.detected = false } get order () { // always make the detected issue appear first return this.detected ? 0 : this.content.order } getSummaryTitle () { if (this.detected) return `Doctor has found ${this.title}:` return `Doctor has not found evidence of ${this.title}.` + ' When such issues are present:' } getSummary () { return this.content.getSummary() } hasSummary () { return this.content.hasSummary() } getReadMore () { return this.content.getReadMore() } hasReadMore () { return this.content.hasReadMore() } } class Recomendation extends EventEmitter { constructor () { super() this.readMoreOpened = false this.panelOpened = false this.selectedCategory = 'unknown' // wrap content with selected and detected properties this.recommendations = new Map() this.recommendationsAsArray = [] for (const categoryContent of categories.asArray()) { const wrapper = new RecomendationWrapper(categoryContent) this.recommendations.set(wrapper.category, wrapper) this.recommendationsAsArray.push(wrapper) } // create HTML structure this.space = d3.select('#recommendation-space') this.container = d3.select('#recommendation') .classed('open', this.panelOpened) this.details = this.container.append('div') .classed('details', true) this.menu = this.details.append('div') .classed('menu', true) this.content = this.details.append('div') .classed('content', true) .on('scroll.scroller', () => this._drawSelectedArticleMenu()) this.summaryTitle = this.content.append('div') .classed('summary-title', true) this.summary = this.content.append('div') .classed('summary', true) this.readMoreButton = this.content.append('div') .classed('read-more-button', true) .on('click', () => this.emit(this.readMoreOpened ? 'close-read-more' : 'open-read-more')) this.readMore = this.content.append('div') .classed('read-more', true) this.articleMenu = this.readMore.append('nav') .classed('article-menu', true) this.readMoreArticle = this.readMore.append('article') .classed('article', true) this.pages = this.menu.append('ul') const pagesLiEnter = this.pages .selectAll('li') .data(this.recommendationsAsArray, (d) => d.category) .enter() .append('li') .classed('recommendation-tab', true) .on('click', (d) => this.emit('menu-click', d.category)) pagesLiEnter.append('span') .classed('menu-text', true) .attr('data-content', (d) => d.menu) pagesLiEnter.append('svg') .classed('warning-icon', true) .call(icons.insertIcon('warning')) // Add button to show-hide tabs described undetected issues this.pages.append('li') .classed('show-hide', true) .on('click', () => this.emit(this.undetectedOpened ? 'close-undetected' : 'open-undetected')) .append('span') .classed('menu-text', true) this.menu.append('svg') .classed('close', true) .on('click', () => this.emit('close-panel')) .call(icons.insertIcon('close')) this.bar = this.container.append('div') .classed('bar', true) .on('click', () => this.emit(this.panelOpened ? 'close-panel' : 'open-panel')) this.bar.append('div') .classed('text', true) const arrow = this.bar.append('div') .classed('arrow', true) arrow.append('svg') .classed('arrow-up', true) .call(icons.insertIcon('arrow-up')) arrow.append('svg') .classed('arrow-down', true) .call(icons.insertIcon('arrow-down')) } setData (data) { this.defaultCategory = data.analysis.issueCategory this.recommendations.get(this.defaultCategory).detected = true // reorder pages, such that the detected page selector comes first this.pages .selectAll('li.recommendation-tab') .sort((a, b) => a.order - b.order) // set the default page this.setPage(this.defaultCategory) } setPage (newCategory) { const oldCategory = this.selectedCategory this.selectedCategory = newCategory this.recommendations.get(oldCategory).selected = false this.recommendations.get(newCategory).selected = true } draw () { this.pages .selectAll('li.recommendation-tab') .data(this.recommendationsAsArray, (d) => d.category) .classed('detected', (d) => d.detected) .classed('selected', (d) => d.selected) .classed('has-read-more', (d) => d.hasReadMore()) const recommendation = this.recommendations.get(this.selectedCategory) // update state classes this.container .classed('open', this.panelOpened) .classed('read-more-open', this.readMoreOpened) .classed('undetected-opened', this.undetectedOpened) .classed('has-read-more', recommendation.hasReadMore()) // set content this.summaryTitle.text(recommendation.getSummaryTitle()) this.summary.html(null) if (recommendation.hasSummary()) { this.summary.node().appendChild(recommendation.getSummary()) } this.readMoreArticle.html(null) this.articleMenu.html(null) if (recommendation.hasReadMore()) { this.readMoreArticle.node().appendChild(recommendation.getReadMore()) this.articleMenu.append('h2') .text('Jump to section') this.articleMenu.append('ul') .selectAll('li') .data(this.readMoreArticle.selectAll('h2').nodes()) .enter() .append('li') .text((headerElement) => headerElement.textContent) .on('click', function (headerElement) { headerElement.scrollIntoView({ behavior: 'smooth', block: 'start' }) }) this._drawSelectedArticleMenu() } // set space height such that the fixed element don't have to hide // something in the background. this.space.style('height', this.details.node().offsetHeight + 'px') } _drawSelectedArticleMenu () { const contentScrollTop = this.content.node().scrollTop const contentClientHeight = this.content.node().clientHeight function isAboveScrollBottom (headerElement) { const elementBottom = headerElement.offsetTop + headerElement.clientHeight const relativeTopPosition = elementBottom - contentScrollTop return relativeTopPosition <= contentClientHeight } const selection = this.articleMenu.select('ul').selectAll('li') const mostRecentHeader = selection.data() .filter(isAboveScrollBottom) .pop() selection.classed('selected', function (headerElement) { return headerElement === mostRecentHeader }) } openPanel () { this.panelOpened = true } closePanel () { this.panelOpened = false } openReadMore () { this.readMoreOpened = true } closeReadMore () { this.readMoreOpened = false } openUndetected () { this.undetectedOpened = true } closeUndetected () { this.undetectedOpened = false } } module.exports = new Recomendation()