Skip to content

Instantly share code, notes, and snippets.

@skipjac
Created February 12, 2018 20:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save skipjac/18f296ca99cfa25e7f8f376868cc8e72 to your computer and use it in GitHub Desktop.
Save skipjac/18f296ca99cfa25e7f8f376868cc8e72 to your computer and use it in GitHub Desktop.
video library in Zendesk Guide
<div class="article-main" data-section-id="{{section.id}}" data-section-name="{{section.name}}">
<div id="sidebar" class="sidebar-panel">
<ul class="sidebar">
<div class="close-sidebar">←</div>
<li class="sidebar-item sidebar-home">
<a href="{{page_path 'help_center'}}" class="sidebar-item-title">
Home
</a>
</li>
<li :class="['sidebar-item', 'sidebar-section', isOpen(category.id)]"
v-for='category in categories'>
<h4 class="sidebar-item-title">
<div v-show="category.id === activeCategory"
v-cloak>
{[{category.name}]}
</div>
<div v-show="category.id !== activeCategory">
<a :href="category.url"
v-cloak>
{[{category.name}]}
</a>
</div>
</h4>
<ul v-show='category.id === activeCategory'>
<div class="group-name" v-show="isCategoryGroup(category.id)">
<div
v-for="(name, groupname) in categoryGroups[category.id]"
class="sidebar-group-name">
<h4>{[{groupname}]}</h4>
<li
:class="['sidebar-item', isArticleInSection(section.id)]"
v-for="section in categoryGroups[category.id][groupname]"
:data-id='section.id'>
<a
class="sidebar-item-link"
:href="section.url">
{[{section.name}]}
</a>
</li>
</div>
</div>
<div v-show="!isCategoryGroup(category.id)">
<li
:class="['sidebar-item', isArticleInSection(section.id)]"
v-for="section in category.sections"
:data-id="section.id">
<a
class="sidebar-item-link"
:href="section.url">
{[{section.name}]}
</a>
</li>
</div>
</ul>
</li>
</ul>
</div>
<div class="content article">
<div class="container">
<span class="open-sidebar hide">☰</span>
<nav class="sub-nav" data-section-name="{{section.name}}">
{{breadcrumbs}}
</nav>
<div class="article-container" id="article-container">
<article class="article">
<header class="article-header">
<h1 title="{{article.title}}" class="article-title">
{{article.title}}
{{#if article.internal}}
<span class="icon-lock" title="{{t 'internal'}}"></span>
{{/if}}
</h1>
<div class="article-author">
<div class="avatar article-avatar">
{{#if article.author.agent}}
<span class="icon-agent"></span>
{{/if}}
<img src="{{article.author.avatar_url}}" alt="Avatar" class="user-avatar"/>
</div>
<div class="article-meta">
{{#link 'user_profile' id=article.author.id}}
{{article.author.name}}
{{/link}}
<ul class="meta-group">
{{#is article.created_at article.updated_at}}
<li class="meta-data">{{date article.created_at timeago=true}}</li>
{{else}}
<li class="meta-data">{{date article.updated_at timeago=true}}</li>
<li class="meta-data">{{t 'updated'}}</li>
{{/is}}
</ul>
</div>
</div>
{{subscribe}}
</header>
<section class="article-info">
<div class="article-content">
<div class="article-body">{{article.body}}</div>
<div class="article-attachments">
<ul class="attachments">
{{#each attachments}}
<li class="attachment-item">
<a href="{{url}}" target="_blank">{{name}}</a>
<div class="attachment-meta meta-group">
<span class="attachment-meta-item meta-data">{{size}}</span>
<a href="{{url}}" target="_blank" class="attachment-meta-item meta-data">Download</a>
</div>
</li>
{{/each}}
</ul>
</div>
</div>
</section>
{{#is section.articles.length 1}}
{{else}}
<div class="article-footer-nav">
<div class="article-nav-box">
<a v-if="nav.prev" :href="nav.prev.url" class="article-nav article-nav-prev">
<span class="article-nav-label">Prev</span>
<span class="article-nav-title">{[{nav.prev.title}]}</span>
</a>
</div>
<div class="article-nav-box">
<a v-if="nav.next" :href="nav.next.url" class="article-nav article-nav-next">
<span class="article-nav-label">Next</span>
<span class="article-nav-title">{[{nav.next.title}]}</span>
</a>
</div>
</div>
{{/is}}
<footer>
<div class="article-footer">
{{#if comments}}
<a href="#article-comments" class="article-comment-count">
<span class="icon-comments"></span>
{{article.comment_count}}
</a>
{{/if}}
</div>
{{#with article}}
<div class="article-votes">
<span class="article-votes-question">{{t 'was_this_article_helpful'}}</span>
<div class="article-votes-controls" role='radiogroup'>
{{vote 'up' role='radio' class='button article-vote article-vote-up'}}
{{vote 'down' role='radio' class='button article-vote article-vote-down'}}
</div>
<small class="article-votes-count">
{{vote 'label' class='article-vote-label'}}
</small>
</div>
{{/with}}
<div class="article-more-questions">
{{request_callout}}
</div>
<div class="article-return-to-top">
<a href="#article-container">{{t 'return_to_top'}}<span class="icon-arrow-up"></span></a>
</div>
</footer>
<section class="article-relatives">
{{recent_articles}}
{{related_articles}}
</section>
<div class="article-comments" id="article-comments">
<section class="comments">
<header class="comment-overview">
<h3 class="comment-heading">
{{t 'comments'}}
</h3>
<p class="comment-callout">{{t 'comments_count' count=article.comment_count}}</p>
{{#if comments}}
<div class="dropdown comment-sorter">
<a class="dropdown-toggle">{{t 'sort_by'}}</a>
<span class="dropdown-menu" role="menu">
{{#each comment_sorters}}
<a aria-selected="{{selected}}" href="{{url}}" role="menuitem">{{name}}</a>
{{/each}}
</span>
</div>
{{/if}}
</header>
<ul id="comments" class="comment-list">
{{#each comments}}
<li id="{{anchor}}" class="comment">
<div class="comment-wrapper">
<div class="comment-info">
<div class="comment-author">
<div class="avatar comment-avatar">
{{#if author.agent}}
<span class="icon-agent"></span>
{{/if}}
<img src="{{author.avatar_url}}" alt="Avatar" class="user-avatar"/>
</div>
<div class="comment-meta">
<span title="{{author.name}}">
{{#link 'user_profile' id=author.id}}
{{author.name}}
{{/link}}
</span>
<ul class="meta-group">
{{#if editor}}
<li class="meta-data">{{date edited_at timeago=true}}</li>
<li class="meta-data">{{t 'edited'}}</li>
{{else}}
<li class="meta-data">{{date created_at timeago=true}}</li>
{{/if}}
</ul>
</div>
<div class="comment-labels">
{{#with ticket}}
<a href="{{url}}" target="_zendesk_lotus" class="status-label escalation-badge">
{{t 'request'}}{{id}}
</a>
{{/with}}
{{#if pending}}
<span class="comment-pending status-label status-label-pending">{{t 'pending_approval'}}</span>
{{/if}}
</div>
</div>
<section class="comment-body">{{body}}</section>
</div>
<div class="comment-actions-container">
<div class="comment-vote vote" role='radiogroup'>
{{vote 'up' role='radio' class='vote-up' selected_class='vote-voted'}}
{{vote 'sum' class='vote-sum'}}
{{vote 'down' role='radio' class='vote-down' selected_class='vote-voted'}}
</div>
<div class="comment-actions actions">
{{actions}}
</div>
</div>
</div>
</li>
{{/each}}
</ul>
{{pagination}}
{{#form 'comment' class='comment-form'}}
<div class="avatar comment-avatar">
{{user_avatar class='user-avatar'}}
</div>
<div class="comment-container">
{{wysiwyg 'body'}}
<div class="comment-form-controls">
{{input type='submit'}}
</div>
</div>
{{/form}}
<p class="comment-callout">{{comment_callout}}</p>
</section>
</div>
</article>
<section class="article-sidebar other-articles">
<!-- Articles in Section -->
<section class="section-articles collapsible-sidebar">
<h3 class="collapsible-sidebar-title sidenav-title">{{t 'articles_in_section'}}</h3>
<ul>
{{#each section.articles}}
<li>
<a href="{{url}}" class="sidenav-item {{#is id ../article.id}}current-article{{/is}}">{{title}}</a>
</li>
{{/each}}
</ul>
{{#if section.more_articles}}
<a href="{{section.url}}" class="article-sidebar-item">{{t 'see_more'}}</a>
{{/if}}
</section>
</section>
<section class="article-sidebar">
<!-- Related Articles -->
<section class="section-articles collapsible-sidebar">
<h3 class="collapsible-sidebar-title sidenav-title">{{t 'related_articles'}}</h3>
<ul v-for="(article, idx) in relatedArticles">
<li v-if="idx < 6">
<a href="{{article.url}}" class="sidenav-item">{{article.title}}</a>
</li>
</ul>
</section>
</section>
<section class="article-sidebar">
<!-- Related Videos Carousel -->
<section class="section-articles collapsible-sidebar">
<h3 class="collapsible-sidebar-title sidenav-title">Related videos</h3>
<div class="carousel-container">
<div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel">
<ol class="carousel-indicators">
<li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li>
<li data-target="#carouselExampleIndicators" data-slide-to="1"></li>
<li data-target="#carouselExampleIndicators" data-slide-to="2"></li>
</ol>
<div class="carousel-inner" role="listbox">
<div class="carousel-item active">
<div class="carousel-image-container">
<img class="d-block img-fluid" src="//p13.zdassets.com/hc/theme_assets/1953888/115000019131/1.jpg" alt="First slide">
</div>
<div class="carousel-caption d-none d-md-block">
<h3>First Video</h3>
<p>First video description</p>
</div>
</div>
<div class="carousel-item">
<div class="carousel-image-container">
<img class="d-block img-fluid" src="//p13.zdassets.com/hc/theme_assets/1953888/115000019131/2.jpg" alt="Second slide">
</div>
<div class="carousel-caption d-none d-md-block">
<h3>Second Video</h3>
<p>Second video description</p>
</div>
</div>
<div class="carousel-item">
<div class="carousel-image-container">
<img class="d-block img-fluid" src="//p13.zdassets.com/hc/theme_assets/1953888/115000019131/3.jpg" alt="Third slide">
</div>
<div class="carousel-caption d-none d-md-block">
<h3>Third Video</h3>
<p>Third video description</p>
</div>
</div>
</div>
<a class="carousel-control-prev" href="#carouselExampleIndicators" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#carouselExampleIndicators" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
</div>
</section>
</section>
</div>
<div class="request">
<h3>Can't find what you're looking for?</h3>
<a class="button" href="{{page_path 'new_request'}}">
Submit a request
</a>
</div>
</div>
</div>
</div>
<script>
sidebar.$mount("#sidebar");
HC.templates.article.$mount(".content.article");
</script>
<div id="sidebar" class="sidebar-panel">
<ul class="sidebar">
<div class="close-sidebar">←</div>
<li class="sidebar-item sidebar-home">
<a href="{{page_path 'help_center'}}" class="sidebar-item-title">
Home
</a>
</li>
<li :class="['sidebar-item', 'sidebar-section', isOpen(category.id)]"
v-for='category in categories'>
<h4 class="sidebar-item-title">
<div v-show="category.id === activeCategory"
v-cloak>
{[{category.name}]}
</div>
<div v-show="category.id !== activeCategory">
<a :href="category.url"
v-cloak>
{[{category.name}]}
</a>
</div>
</h4>
<ul v-show="category.id === activeCategory">
<div class="group-name" v-if="isCategoryGroup(category.id)">
<div
v-for="(name, key) in categoryGroups[category.id]"
class="sidebar-group-name"
v-cloak>
<h4>{[{key}]}</h4>
<li
class="sidebar-item"
v-for="section in categoryGroups[category.id][key]"
:data-id='section.id'>
<a
class="sidebar-item-link"
:href="section.url"
v-cloak>
{[{section.name}]}
</a>
</li>
</div>
</div>
<div v-else>
<li
class="sidebar-item"
v-for="section in category.sections"
:data-id="section.id">
<a
class="sidebar-item-link"
:href="section.url"
v-cloak>
{[{section.name}]}
</a>
</li>
</div>
</ul>
</li>
</ul>
</div>
<div class="content category">
<div class="container">
<span class="open-sidebar hide">☰</span>
<nav class="sub-nav">
{{breadcrumbs}}
</nav>
<section class="category-content">
<header class="page-header">
<h1>{{category.name}}</h1>
{{#if category.description}}
<p class="page-header-description">
{{category.description}}
</p>
{{/if}}
</header>
<div v-show="{{category.id}} !== HC.SETTINGS.videos.videoLibraryCategoryId">
<div class="section-group" v-for="(group, title) in groups" >
<h2 v-if="title"
class="section-group-title open"
@click="toggleGroup"
v-cloak>
{[{title}]}
</h2>
<ul class="blocks-list category">
<li class="blocks-item category" v-for="section in group">
<a :href="section.url" class="blocks-item-link">
<div v-if="hasIcon(section.id)">
<net-icon :id="section.id"></net-icon>
</div>
<div v-else>
<div class="img-holder"></div>
</div>
<h4 class="blocks-item-title" v-cloak>
{[{section.title}]}
<span v-if="section.internal" class="icon-lock" title="{{t 'internal'}}"></span>
</h4>
<p v-cloak>{[{getIconDescription(section.id)}]}</p>
</a>
</li>
<!-- Filler block for consistent flex grid -->
<li class="blocks-item"></li>
<li class="blocks-item"></li>
</ul>
</div>
</div>
<div v-show="{{category.id}} == HC.SETTINGS.videos.videoLibraryCategoryId">
<ul class="blocks-list">
<li class="video-group" v-for="section in videoSections">
<a :href="section.html_url" class="video-section-title-link">
<h2 class="group-title" v-cloak>
{[{section.name}]}
</h2>
</a>
<p class="video-section-description" v-cloak>
{[{section.description}]}
</p>
<div class="modal">
<div v-for="(article, index) in section.articles">
<div v-if="index<6" class="video-wrapper-section">
<net-thumbnail :article="article" :open-modal="setCurrentVideo" v-cloak></net-thumbnail>
</div>
</div>
<div v-if="section.articles.length>6" class="video-wrapper-section">
<a :href="section.html_url" class="more-videos">Click here for more videos</a>
</div>
</div>
<!-- Filler block for consistent flex grid -->
<li class="blocks-item"></li>
<li class="blocks-item"></li>
</ul>
</div>
</section>
</div>
<net-video v-if="currentVideo" :article="currentVideo" :close-modal="setCurrentVideo" v-cloak></net-video>
</div>
<!-- for JS to fetch sections using built-in curlybar calls -->
<div id="section-data" style="display:none;">
{{#each sections}}
<section class="section" data-id="{{id}}" data-title="{{name}}" data-url="{{url}}" data-internal="{{internal}}">{{description}}</section>
{{else}}
<i class="category-empty">
<a href="{{category.url}}">{{t 'empty'}}</a>
</i>
{{/each}}
</div>
<script>
sidebar.$mount("#sidebar");
HC.templates.category.$mount(".content.category");
</script>
var HC = HC || {};
HC.templates = {};
var Utils = HC_Utils();
var Store = HC_Store();
// Fetch and set local settings
HC.LOCALE = Utils.getLocale(window.location.pathname);
// HC.isMobile = (new MobileDetect(window.navigator.userAgent)).phone();
Store.init();
// Custom delimiter for Vue templates
Vue.options.delimiters = ['{[{', '}]}'];
Vue.component('net-video', {
template: '<div v-if="isModalOpen" class="article-lightbox"><div class="close-lightbox" @click="closeModal(null)"></div><div class="constrain"><div class="article-lightbox-wrapper"><div class="article-body"><h1 class="article-title">{[{article.title}]}</h1><div v-html="videoUrl" class="video"></div></div></div></div>',
props: ['article', 'closeModal'],
data: function() {
return {
id: null,
scriptUrl: null,
video: null
}
},
created: function() {
this.scriptUrl = $(this.article.body).find("script:first-child").attr("src");
this.id = this.scriptUrl.split("/").pop().split(".")[0];
},
mounted: function() {
$.getScript(this.scriptUrl, function(){
if (this.id) {
this.video = Wistia.api(this.id);
if (this.video) this.video.play();
}
}.bind(this));
},
beforeDestroy: function() {
if (this.video) this.video.remove();
},
destroyed: function() {
if (window.location.href.indexOf("#videos?id=") > -1) {
window.history.back();
}
},
computed: {
isModalOpen: function() {
return this.article !== null;
},
videoUrl: function() {
return this.article && this.article.body.match(/<div[\S\s]*<\/div>/)[0].replace(/[\n]/g, '');
},
},
});
Vue.component('net-thumbnail', {
template: '<div class="modal-wrapper"><div class="screenshot-overlay" @click="openModal(article)"></div><h3 class="article-title-thumb" @click="openModal(article)">{[{article.name}]}</h3><div v-html="screenshotUrl" class="screenshot"></div></div></div>',
props: ['article', 'modal', 'openModal'],
data: function() {
return {
id: null,
screenshotUrl: '<img src="https://embedwistia-a.akamaihd.net/deliveries/2ca5be7e639e0a69bfa7413657dd926858638aec.jpg?image_play_button_size=2x&amp;image_crop_resized=960x540&amp;image_play_button=1&amp;image_play_button_color=54bbffe0"/>'
}
},
mounted: function() {
var redirectedArticleId = window.location.href.split("=")[1];
if (this.article.id == redirectedArticleId) {
this.openModal(this.article);
}
}
});
/*=========================================================
* CATEGORY TEMPLATE
*========================================================= */
HC.templates.category = new Vue({
data: {
sections: [],
videoSections: [],
articles: [],
keys: null,
// hard code video library category id for display purposes
// test: 115000365991
videoLibraryCategoryId: 115001863567,
modalOpenReady: true,
currentVideo: null
},
created: function() {
var url = url || "/api/v2/help_center/" + HC.LOCALE + "/categories/" + this.videoLibraryCategoryId + "/articles.json?per_page=100&include=sections";
$.get(url, function(data){
if (data.count) {
this.videoSections = data.sections;
this.articles = data.articles;
this.mapArticlesToSections(this.articles, this.videoSections);
}
}.bind(this));
},
mounted: function() {
this.fetchSections();
},
computed: {
groups: function() {
console.log(_.groupBy(this.sections, "group") || []);
return _.groupBy(this.sections, "group") || [];
},
},
methods: {
fetchSections: function() {
var self = this;
$("#section-data .section").each(function(){
var id = $(this).data("id"),
url = $(this).data("url"),
rawTitle = $(this).data("title"),
internal = $(this).data("internal"),
description = $(this).text().trim(),
index = rawTitle.indexOf(":"),
groupTitle = index > -1 ? rawTitle.substring(0, index).trim() : "",
sectionTitle = index > -1 ? rawTitle.substring(index + 1, rawTitle.length).trim() : rawTitle;
self.sections.push({
id: id,
title: sectionTitle,
group: groupTitle,
rawTitle: rawTitle,
url: url,
internal: internal,
description: description
});
});
},
mapArticlesToSections: function(articles, sections) {
var articleGroups = _.groupBy(articles, "section_id");
_.each(sections, function(section){
section.articles = articleGroups[section.id];
}, this);
},
getIconDescription: function(id) {
return HC.SETTINGS.iconDescriptions[id];
},
setCurrentVideo: function(article) {
this.currentVideo = article;
},
hasIcon: function(sectionId) {
this.keys = this.keys || _.keys(HC.SETTINGS.icons);
return this.keys.indexOf(sectionId.toString()) > -1;
},
toggleGroup: function(e) {
if (e.target.classList.contains("open")) {
// if group already open, close it and hide sections
$(e.target).removeClass("open").addClass("close");
$(e.target).next("ul").addClass("hide");
} else {
// otherwise reveal sections
$(e.target).removeClass("close").addClass("open");
$(e.target).next("ul").removeClass("hide");
}
}
}
});
/*=========================================================
* SECTION TEMPLATE
*========================================================= */
HC.templates.section = new Vue({
data: {
prefix: "group_",
refPrefix: "ref_",
sectionId: null,
articles: [],
activeGroup: "",
sectionTitle: null,
isAccordion: false,
showAllArticles: false,
sectionHasVideos: false,
videoSections: [],
videoArticles: [],
// test: 115000849831, 115000797932
sectionsInVideos: [115003240567, 115002781767, 115003387968],
currentVideo: null
},
created: function() {
// check if was redirected to section page from video article page
if (window.location.href.indexOf("#videos?id=") > -1) {
var urlWithoutArticle = window.location.href.split("#")[0];
this.sectionId = Utils.getPageId(urlWithoutArticle);
} else {
this.sectionId = Utils.getPageId(window.location.href);
}
for (var i=0; i<this.sectionsInVideos.length; i++) {
if (window.location.href.indexOf(this.sectionsInVideos[i]) > -1) {
this.sectionHasVideos = true;
}
}
},
mounted: function() {
if (this.sectionHasVideos) {
this.fetchVideoArticles(this.sectionId);
} else {
this.fetchArticles(this.sectionId);
}
// Get section name without :
var name = $("#section-data section").data("name");
this.sectionTitle = name.split(": ")[1] ? name.split(": ")[1] : name;
// Breadcrumb displays correctly formatted name without group prefix
$(".breadcrumbs li:last-child").text(this.sectionTitle);
} ,
computed: {
/**
* Groups of articles based on article group label
* @return {Array} array of grouped articles
*/
groups: function(){
var articles = [],
groups = [],
// Get group label for each article
articles = _.map(this.articles, this.getGroupLabel.bind(this));
// Group articles by groupLabel
if (articles) groups = _.groupBy(articles, "groupLabel");
return groups || [];
}
},
methods: {
/**
* Make API call to articles by section ID
* @param {Integer} section ID
* @param {String} url url for next page
*/
fetchArticles: function(sectionId, url) {
var url = url || "/api/v2/help_center/" + HC.LOCALE + "/sections/" + sectionId + "/articles.json?per_page=100";
var urlForRef = "/api/v2/help_center/" + HC.LOCALE + "/articles.json?label_names=ref_" + sectionId;
$.get(url, function(data){
if (data.count) {
this.articles = this.articles.concat(data.articles);
if (data.next_page) {
this.fetchArticles(null, data.next_page + "&per_page=100");
} else {
setTimeout(function() {
$(".request").removeClass("hide");
}, 100);
this.$forceUpdate(); // Required to update article list view
}
} else {
$(".request").removeClass("hide");
}
}.bind(this));
$.get(urlForRef, function(data){
if (data.count) {
this.articles = this.articles.concat(data.articles);
if (data.next_page) {
this.fetchArticles(null, data.next_page + "&per_page=100");
} else {
this.$forceUpdate(); // Required to update article list view
}
}
}.bind(this));
},
fetchVideoArticles: function(sectionId, url) {
var url = url || "/api/v2/help_center/" + HC.LOCALE + "/sections/" + sectionId + "/articles.json?per_page=100&include=sections";
$.get(url, function(data){
if (data.count) {
this.videoArticles = this.videoArticles.concat(data.articles);
this.articles = this.videoArticles;
if (data.next_page) {
this.fetchVideoArticles(null, data.next_page + "&per_page=100");
} else {
setTimeout(function() {
$(".request").removeClass("hide");
}, 100);
this.$forceUpdate();
}
} else {
$(".request").removeClass("hide");
}
}.bind(this));
},
// findVideoSection: function(id) {
// console.log(this.videoSections);
// return _.find(this.videoSections, function(section) {
// return id === section.id;
// });
// },
getGroupLabel: function(article) {
var labels = article.label_names,
groupLabel = _.find(labels, function(label) { return label.indexOf(this.prefix) === 0; }, this);
if (groupLabel) {
article.groupLabel = groupLabel;
} else {
article.groupLabel = "group_"; //Removed Placeholder text - fail to nothing
}
return article;
},
toggleAccordion: function(event) {
if (this.isAccordion) {
var title = $(event.target).text(),
$group = $(event.target.nextElementSibling);
$group.slideToggle();
this.activeGroup = this.activeGroup === title ? "" : title;
}
},
mapArticlesToSections: function(articles, sections) {
var articleGroups = _.groupBy(articles, "section_id");
_.each(sections, function(section){
section.articles = articleGroups[section.id];
}, this);
},
/**
* Extricate article group title from article label
* @param {String} str article label
* @return {} [description]
*/
getGroupTitle: function(str) {
if (str) {
var prefix = this.prefix,
result = str.substring(prefix.length, str.length);
return result.replace("_", " ");
} else {
return "";
}
},
seeAllArticles: function() {
if (this.showAllArticles == true) {
this.showAllArticles = false;
} else {
this.showAllArticles = true;
$(".more-articles").slideDown();
}
},
setCurrentVideo: function(article) {
this.currentVideo = article;
}
}
});
/*=========================================================
* ARTICLE TEMPLATE
*========================================================= */
HC.templates.article = new Vue({
data: {
// test: 115000849831, 115000797932
sectionsInVideos: [115003240567, 115002781767, 115003387968],
id: null,
articles: [],
sections: [],
nav: {
prev: null,
next: null,
}
},
created: function() {
Store.subscribe(["articles", "sections"], function(data){
this.articles = data.articles;
this.sections = data.sections;
}.bind(this));
},
mounted: function() {
this.id = Utils.getPageId(window.location.href);
var sectionId = $(".article-main").data("section-id");
if (this.sectionsInVideos.indexOf(sectionId) == -1) {
$(".article-main").addClass("show");
// carousel
$(".carousel").carousel();
} else {
window.location.replace("/hc/sections/" + sectionId + "#videos?id=" + this.id);
};
// Get section name without :
var name = $(".article-main").data("section-name");
this.articleTitle = name.split(": ")[1] ? name.split(": ")[1] : name;
// Breadcrumb displays correctly formatted name without group prefix
$(".breadcrumbs li:last-child").text(this.articleTitle);
this.setNavLinks(this.sections, this.id);
},
methods: {
setNavLinks: function(sections, id) {
var currArticle = _.find(this.articles, {id: id}),
currSection,
currArticleIndex;
if (currArticle) {
var currSection = _.find(this.sections, {id: currArticle.section_id});
}
if (currSection) {
var currArticleIndex = _.findIndex(currSection.articles, {id: currArticle.id});
}
if (currArticleIndex !== undefined) {
this.nav.prev = currArticleIndex > 0 ? currSection.articles[currArticleIndex - 1] : null;
this.nav.next = currArticleIndex < currSection.articles.length ? currSection.articles[currArticleIndex + 1] : null;
}
},
}
});
/*=========================================================
* UTILITY METHODS
*========================================================= */
function HC_Utils() {
return {
getPageId: function(url) {
var links = url.split("/"),
page = links[links.length - 1],
result = page.split("-")[0];
return parseInt(result,10) || null;
},
// /**
// * Get url parameter value
// * @param {String} name parameter key name
// * @param {String} url
// * @return {String} value of parameter
// */
// getUrlParameter: function(name, url) {
// url = url || location.search;
// name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
// var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
// results = regex.exec(url);
// return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
// },
/**
* Get locale of current page
* @param {String} url url to check for locale code
* @return {String} locale code
*/
getLocale: function(url) {
var links = url.split("/"),
hcIndex = links.indexOf("hc"),
links2 = links[hcIndex + 1].split("?"),
locale = links2[0];
return locale;
},
}
}
function HC_Store() {
return {
content: {
articles: {pagesLeft: null, doneLoading: false, stored: [], data: [], pages: {}},
sections: {pagesLeft: null, doneLoading: false, stored: [], data: []},
categories: {pagesLeft: null, doneLoading: false, stored: [], data: []},
videos: {pagesLeft: null, doneLoading: false, stored: [], data: []},
},
init: function() {
var locale = this.get("locale");
// Reset HC storage if different locale
if (locale !== HC.LOCALE) {
this.clearAll();
store.set("hc_store:locale", HC.LOCALE);
}
var articleCache = this.get("articles") || [],
videoCache = this.get("videos") || [],
sectionCache = this.get("sections") || [],
categoryCache = this.get("categories") || [],
last_updated = this.get("timestamp") || [],
now = new Date().getTime(),
timeDiffInMin = last_updated ? (now - last_updated)/1000/60 : null;
// Fetch categories/sections/articles if cached data doesn't exist or is over 2 hours old
if (!(last_updated && articleCache.length && sectionCache.length && categoryCache.length && videoCache.length) || (timeDiffInMin && timeDiffInMin > 120*1)) {
store.set("hc_store:timestamp", now);
store.set("hc_store:locale", HC.LOCALE);
this.fetch("/api/v2/help_center/" + HC.LOCALE + "/articles.json?per_page=1", this.onAsyncCheckFetch.bind(this));
this.fetch("/api/v2/help_center/" + HC.LOCALE + "/sections.json?include=categories&per_page=100", this.onSectionFetch.bind(this));
}
},
get: function(type) {
if (type === "timestamp" || type === "locale") {
return store.get("hc_store:" + type);
} else {
var data = this.content[type].stored; // Grab previously unpacked data, if any
if (!data.length) {
var cachedData = store.get("hc_store:" + type);
data = cachedData ? this.unpack(cachedData) : [];
this.content[type].stored = data;
}
return data;
}
},
fetch: function(url, callback) {
$.get(url).done(function(data) { if (data.count) callback(data); });
},
onAsyncCheckFetch: function(data) {
var url = data.next_page + "&per_page=100",
pageNum = 1,
pageCount = Math.ceil(data.page_count/100);
this.content.articles.pagesLeft = pageCount;
// asynchronously load the rest of data
while (pageNum <= pageCount) {
var getUrl = url + "&page=" + pageNum;
this.fetch(getUrl, this.onAsyncArticleFetch.bind(this));
pageNum++;
}
},
onAsyncArticleFetch: function(data) {
// Remove article drafts
var articles = _.reject(data.articles, function(article){return article.draft;});
this.content.articles.pages[data.page] = articles;
// Check when done loading all pages
if (this.content.articles.pagesLeft && this.content.articles.pagesLeft > 0) {
this.content.articles.pagesLeft--;
if (this.content.articles.pagesLeft === 0) {
this.content.articles.data = _.flatten(_.values(this.content.articles.pages));
this.cacheArticles(this.content.articles.data);
}
}
},
cacheArticles: function(articles) {
// Process article and videos (hardcoded section IDs)
var articleList = this.stripArticles(articles);
var videoList = _.filter(articles, function(article){ return _.contains([115003240567, 115002781767, 115003387968], article.section_id) });
if (videoList.length > 0) {
videoList = this.stripVideos(videoList);
}
// Update content
this.content.articles.data = articleList;
this.content.videos.data = videoList;
// Trigger loading complete
this.content.articles.doneLoading = true;
this.content.videos.doneLoading = true;
// Compress data
var minArticleList = this.pack(articleList);
var minVideoList = this.pack(videoList);
// Cache data
store.set("hc_store:articles", minArticleList);
store.set("hc_store:videos", minVideoList);
},
onSectionFetch: function(data) {
if (data.count) {
this.content.categories.data.push.apply(this.content.categories.data, data.categories);
this.content.sections.data.push.apply(this.content.sections.data, data.sections);
if (data.next_page) {
var url = data.next_page + "&per_page=100";
this.fetch(url, this.onSectionFetch.bind(this));
} else {
this.cacheSections(this.content.categories.data, this.content.sections.data);
}
}
},
cacheSections: function(categories, sections) {
// Process data
var processedCategories = this.stripCategories(categories);
var processedSections = this.stripSections(sections);
// Update content data
this.content.categories.data = processedCategories;
this.content.sections.data = processedSections;
// Trigger loading complete
this.content.categories.doneLoading = true;
this.content.sections.doneLoading = true;
// Compress data
var minCategories = this.pack(processedCategories);
var minSections = this.pack(processedSections);
// Store in browser cache
store.set("hc_store:categories", minCategories);
store.set("hc_store:sections", minSections);
},
clearAll: function() {
store.remove("hc_store:categories");
store.remove("hc_store:sections");
store.remove("hc_store:articles");
store.remove("hc_store:videos");
store.remove("hc_store:timestamp");
store.remove("hc_store:locale");
},
stripCategories: function(categoryList) {
var results = [],
flags = {},
categories;
categories = _.sortBy(categoryList, function(category){return category.position; });
_.each(categories, function(category){
var isDuplicate = flags[category.id];
if (!isDuplicate) {
flags[category.id] = true;
results.push({
id: category.id,
name: category.name,
position: category.position,
description: category.description,
url: category.html_url,
});
}
});
results = _.sortBy(results, "position");
return results;
},
stripSections: function(sectionList) {
var results = [],
flags = {};
_.each(sectionList, function(section){
var isDuplicate = flags[section.id];
if (!isDuplicate) {
flags[section.id] = true;
results.push({
id: section.id,
name: section.name,
position: section.position,
category_id: section.category_id,
url: section.html_url,
});
}
});
results = _.sortBy(results, "position");
return results;
},
stripArticles: function(articleList) {
var results = [],
flags = {};
_.each(articleList, function(article){
var isDuplicate = flags[article.id];
if (!isDuplicate) {
flags[article.id] = true;
results.push({
id: article.id,
label_names: article.label_names,
title: article.title,
position: article.position,
section_id: article.section_id,
url: article.html_url,
});
}
});
results = _.sortBy(results, "position");
return results;
},
stripVideos: function(articles) {
return _.map(articles, function(article){
var videoScripts = $(article.body).find("script:first-child").attr("src"),
wistiaId,
videoUrl;
if (videoScripts) {
wistiaId = videoScripts.split("/").pop().split(".")[0];
}
if (article.body) {
videoUrl = article.body.match(/<div[\S\s]*<\/div>/)[0].replace(/[\n]/g, '');
}
return {
id: article.id,
label_names: article.label_names,
title: article.title,
position: article.position,
section_id: article.section_id,
videoScripts: videoScripts,
wistiaId: wistiaId,
videoUrl: videoUrl,
};
}, this);
},
subscribe: function(contentType, callback) {
var hasMultipleType = _.isArray(contentType),
data = hasMultipleType ? this.fetchLocalMultipleData(contentType) : this.fetchLocalSingleData(contentType);
if (typeof callback !== "function") throw "Error: Callback is not a function";
if (data) {
callback(data);
} else {
if (hasMultipleType) {
this.subscribeMultiple(contentType, callback);
} else {
this.subscribeSingle(contentType, callback);
}
}
},
fetchLocalSingleData: function(contentType) {
var data = {};
data[contentType] = this.get(contentType);
return data[contentType].length ? data : null;
},
fetchLocalMultipleData: function(contentTypeArray) {
var allData = {},
allDataExists = true;
_.each(contentTypeArray, function(dataType){
allData[dataType] = this.get(dataType);
if (!allData[dataType].length) allDataExists = false;
}, this);
return allDataExists ? allData : null;
},
subscribeMultiple: function(contentTypeArray, callback) {
for(var i = 0; i < contentTypeArray.length; i++) {
if (!Store.content.hasOwnProperty(contentTypeArray[i])) {
throw "Error: HC_Storage has no such content type: " + contentTypeArray[i];
}
}
var interval = setInterval(function(){
var allDataLoaded = _.every(contentTypeArray, function(dataType){
return Store.content[dataType].doneLoading === true;
});
if (allDataLoaded) {
clearInterval(interval);
var allData = {};
_.each(contentTypeArray, function(dataType){
allData[dataType] = Store.content[dataType].data;
});
callback(allData);
}
}, 100);
// Clear interval if no data loaded within 10 seconds
setTimeout(function(){ clearInterval(interval); }, 5 * 1000);
},
subscribeSingle: function(contentType, callback) {
// make sure data type exist in localStorage
if (!Store.content.hasOwnProperty(contentType)) {
throw "Error: HC_Storage has no such content type: " + contentTypeArray;
}
if (typeof callback === "function") {
var interval = setInterval(function(){
if (Store.content[contentType].doneLoading === true) {
clearInterval(interval);
var data = {};
data[contentType] = Store.content[contentType].data;
callback(data);
}
}, 100);
// Clear interval if no data loaded within 10 seconds
setTimeout(function(){ clearInterval(interval); }, 5 * 1000);
}
},
pack: function(data) {
return LZString.compress(JSON.stringify(data));
},
unpack: function(data) {
return JSON.parse(LZString.decompress(data));
}
}
};
<div id="sidebar" class="sidebar-panel">
<ul class="sidebar">
<div class="close-sidebar">←</div>
<li class="sidebar-item sidebar-home">
<a href="{{page_path 'help_center'}}" class="sidebar-item-title">
Home
</a>
</li>
<li :class="['sidebar-item', 'sidebar-section', isOpen(category.id)]"
v-for='category in categories'>
<h4 class="sidebar-item-title">
<div v-show="category.id === activeCategory"
v-cloak>
{[{category.name}]}
</div>
<div v-show="category.id !== activeCategory">
<a :href="category.url"
v-cloak>
{[{category.name}]}
</a>
</div>
</h4>
<ul v-show='category.id === activeCategory'>
<div class="group-name" v-show="isCategoryGroup(category.id)">
<div
v-for="(name, key) in categoryGroups[category.id]"
class="sidebar-group-name"
v-cloak>
<h4>{[{key}]}</h4>
<li
:class="['sidebar-item', isActive(section.id,category.id)]"
v-for="section in categoryGroups[category.id][key]"
:data-id='section.id'>
<a
class="sidebar-item-link"
:href="section.url"
v-cloak>
{[{section.name}]}
</a>
</li>
</div>
</div>
<div v-show="!isCategoryGroup(category.id)">
<li
:class="['sidebar-item', isActive(section.id,category.id)]"
v-for="section in category.sections"
:data-id='section.id'>
<a
class="sidebar-item-link"
:href="section.url"
v-cloak>
{[{section.name}]}
</a>
</li>
</div>
</ul>
</li>
</ul>
</div>
<div class="content section">
<div class="container">
<span class="open-sidebar hide">☰</span>
<nav class="sub-nav">
{{breadcrumbs}}
</nav>
<section class="section-content">
<header class="page-header">
<div class="section-header">
<h1 v-cloak>
{[{sectionTitle}]}
{{#if section.internal}}
<span class="icon-lock" title="{{t 'internal'}}"></span>
{{/if}}
</h1>
{{subscribe}}
</div>
{{#if section.description}}
<p class="page-header-description">{{section.description}}</p>
{{/if}}
</header>
<div v-show="!sectionHasVideos">
<div class="blocks-list">
<div
:class="['article-group', isAccordion && 'accordion']"
v-for="(group, title) in groups">
<h2
v-if="title.length"
:class="['group-title', activeGroup == title ? 'active' : '']"
@click="toggleAccordion($event)"
v-cloak>
{[{getGroupTitle(title)}]}
</h2>
<div class="group-content">
<ul class="article-list">
<li class="article-list-item" v-for="(article, index) in group">
<a
v-if="index<5"
:href="article.html_url"
class="article-list-link"
v-cloak>
{[{article.title}]}
</a>
</li>
<div v-if="group.length>5">
<div v-show="showAllArticles" class="more-articles">
<li class="article-list-item" v-for="(article, index) in group">
<a v-show="index>4"
:href="article.html_url"
class="article-list-link"
v-cloak>
{[{article.title}]}
</a>
</li>
</div>
<a class="see-all-articles" @click="seeAllArticles" v-cloak>
{[{showAllArticles ? "See fewer articles" : "See all articles"}]}
</a>
</div>
</ul>
</div>
</div>
</div>
</div>
<div v-show="sectionHasVideos">
<div class="video-group" v-for="(group, title) in groups">
<h2 v-if="title.length" :class="group-title" v-cloak>
{[{getGroupTitle(title)}]}
</h2>
<div class="modal">
<div v-for="article in videoArticles"
class="video-wrapper-section">
<net-thumbnail :article="article" :open-modal="setCurrentVideo" v-cloak></net-thumbnail>
</div>
<!-- Filler block for consistent flex grid -->
<li class="blocks-item"></li>
</div>
</div>
</div>
<div class="request hide">
<h3>Can't find what you're looking for?</h3>
<a class="button" href="{{page_path 'new_request'}}">
Submit a request
</a>
</div>
</section>
</div>
<net-video v-if="currentVideo" :article="currentVideo" :close-modal="setCurrentVideo" v-cloak></net-video>
</div>
<!-- for JS to fetch this section info using built-in curlybar calls -->
<div id="section-data" style="display:none;">
<section data-id="{{section.id}}" data-name="{{section.name}}" data-url="{{section.url}}">
{{section.description}}
</section>
</div>
<script>
sidebar.$mount("#sidebar");
HC.templates.section.$mount(".content.section");
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment