docuhelp
Version:
self service knowledgebase software
282 lines (201 loc) • 7.79 kB
JavaScript
const md = require('markdown-it')()
const Fuse = require('fuse.js')
const createMarkup = require('./createMarkup')
function DocuHelp (opts={}){
if(!(this instanceof DocuHelp)){
return new DocuHelp(opts={})
}
let {
headerText="Instant Answers",
articles=[],
sortOptions={
order: 'asc',
sort_field: 'weight'
},
searchOptions={
searchText: 'Search docs'
}
} = opts
this.opts = {
headerText,
articles,
sortOptions,
searchOptions
}
this.parseMarkdown = function(opts){
if(!Array.isArray(opts.articles)) throw new Error('articles must be an array of objects')
let articles = opts.articles.map((article, i) =>{
let rendered = md.render(article.body);
let plainBody = this.toPlainText(rendered)
return {
short_title: this.truncateText(article.title, 120),
title: article.title,
body: rendered,
id: `${article.title.split(' ').join('-')}-${i}`,
plainBody,
excerpt: this.truncateText(plainBody, 200),
weight: article.weight
}
})
return this.sortArticles(articles)
}
this.toPlainText = function(html){
html = html.replace(/<style([\s\S]*?)<\/style>/gi, '');
html = html.replace(/<script([\s\S]*?)<\/script>/gi, '');
html = html.replace(/<\/div>/ig, '\n');
html = html.replace(/<\/li>/ig, '\n');
html = html.replace(/<li>/ig, ' * ');
html = html.replace(/<\/ul>/ig, '\n');
html = html.replace(/<\/p>/ig, '\n');
html = html.replace(/<br\s*[\/]?>/gi, "\n");
html = html.replace(/<[^>]+>/ig, '');
return html
}
this.sortArticles = (articles) => {
let {sort_field, order} = this.opts.sortOptions
function compare(a, b) {
if(!a.hasOwnProperty(sort_field) || !b.hasOwnProperty(sort_field)){
return 0
}
const item1 = typeof a[sort_field] === 'string' ? a[sort_field].toUpperCase() : a[sort_field]
const item2 = typeof b[sort_field] === 'string' ? (b[sort_field]).toUpperCase() : b[sort_field]
let index = 0;
if (item1 > item2) {
index = 1;
} else if (item1 < item2) {
index = -1;
}
return order === 'desc' ? index * -1 : index
}
return articles.sort(compare)
}
this.truncateText = function(text, length=18){
return typeof text === 'string' ?
text.length > length ?
text.substr(0, length) + '...' :
text : text
}
this.opts.articles = this.parseMarkdown(this.opts)
this.searchArticles = function(search_term=''){
return this.fuse.search(search_term)
}
this.searchHandler = function(search_term){
if(!search_term) return
let search_result = this.searchArticles(search_term)
// show matches only
this.opts.articles.map(article => {
document.getElementById(article.id).classList.remove('hidden')
})
for (const item of this.opts.articles) {
let found = search_result.find(a => a.id == item.id)
if(!found){
document.getElementById(item.id).classList.add('hidden')
}
}
// show cancel button
this._cancel_button.classList.remove('hidden')
this._back_button.classList.add('hidden')
}
DocuHelp.prototype.mount = function(){
let markup = createMarkup(this.opts)
document.body.insertAdjacentHTML('beforeend', markup)
this._button = document.getElementById('dh-button')
this._back_button = document.getElementById('dh-back-button')
this._search_button = document.getElementById('dh-search-button')
this._cancel_button = document.getElementById('dh-cancel-button')
this._frame = document.getElementById('dh-frame')
this._cards = document.getElementsByClassName('dh-content-card')
this._search = document.getElementById('dh-search')
this._button.addEventListener('click', (e) => {
this._frame.classList.contains('hidden') ?
this.openWidget() :
this.closeWidget()
})
// button to clear search results
this._cancel_button.addEventListener('click', (e) => {
this.opts.articles.map(article => {
document.getElementById(article.id).classList.remove('hidden')
})
this._cancel_button.classList.add('hidden')
})
// attach listeners to each card
for(let _card of this._cards){
_card.addEventListener('click', (e)=>{
let articleId = _card.getAttribute('id')
let article = this.opts.articles.find(item => item.id == articleId)
this.openArticle(article, articleId)
})
}
// initialize search
let options = {
shouldSort: true,
threshold: 0.4,
tokenize: true,
matchAllTokens: true,
keys: ['title','plainBody'],
...this.opts.searchOptions
}
this.fuse = new Fuse(this.opts.articles, options)
// atach event listener to search input
this._search.addEventListener('keyup', (e) => {
this.search_term = e.target.value
if(e.keyCode == '13'){
e.preventDefault()
let search_term = e.target.value
this.searchHandler(search_term)
}
})
this._search_button.addEventListener('click', (e) => {
this.searchHandler(this.search_term)
})
}
DocuHelp.prototype.destroy = function(){
let elem = document.getElementById('dh-wrapper')
document.body.removeChild(elem)
return this
}
DocuHelp.prototype.openArticle = function(article){
let _article_container = document.getElementById('dh-content-card-open')
let contentCard = `
<div class="dh-content-card__title">
${article.title}
</div>
<div class="dh-content-card__body">
${article.body}
</div>
`
_article_container.innerHTML = contentCard
_article_container.classList.remove('hidden')
_article_container.classList.remove('fadeOutDown')
_article_container.classList.add('fadeInDown')
// toggle back button visibility
let _back_button = document.getElementById('dh-back-button')
_back_button.classList.toggle('hidden')
// hide article
_back_button.addEventListener('click', e=>{
_back_button.classList.toggle('hidden')
e.stopImmediatePropagation()
_article_container.classList.remove('fadeInDown')
_article_container.classList.add('fadeOutDown')
setTimeout(() => {
_article_container.classList.add('hidden')
}, 500);
})
return this
}
DocuHelp.prototype.openWidget = function(){
let elem = document.getElementById('dh-frame')
elem.classList.add('fadeInUp')
elem.classList.remove('fadeOutDown')
elem.classList.remove('hidden')
return this
}
DocuHelp.prototype.closeWidget = function(){
let elem = document.getElementById('dh-frame')
elem.classList.add('fadeOutDown')
elem.classList.remove('fadeInUp')
setTimeout(() => elem.classList.add('hidden'), 1000)
return this
}
}
module.exports = DocuHelp