Implement everything

This commit is contained in:
Mikko Ahlroth 2014-11-15 18:47:23 +02:00
parent 7fe251b7ed
commit b8f0b373a1
21 changed files with 483 additions and 75 deletions

View file

@ -26,12 +26,18 @@ var requireJsRuntimeConfig = vm.runInNewContext(fs.readFileSync('src/app/require
'requireLib',
'lodashLib',
'momentLib',
'components/address-service/address-service',
'components/nav-bar/nav-bar',
'components/home-page/home',
'components/db/db',
'components/generic-route/generic-route',
'components/transfer/transfer',
'components/single-post/single-post',
'components/pagination/pagination'
'components/pagination/pagination',
'components/tag-page/tag-page',
'components/year-page/year-page',
'components/month-page/month-page',
'components/post-page/post-page'
],
insertRequire: ['app/startup'],
bundles: {

View file

@ -1,37 +1,38 @@
define(["knockout", "crossroads", "hasher"], function(ko, crossroads, hasher) {
define(["knockout", "crossroads", "hasher", "./routes"], function(ko, crossroads, hasher, routes) {
// This module configures crossroads.js, a routing library. If you prefer, you
// can use any other routing library (or none at all) as Knockout is designed to
// compose cleanly with external libraries.
//
// You *don't* have to follow the pattern established here (each route entry
// specifies a 'page', which is a Knockout component) - there's nothing built into
// Knockout that requires or even knows about this technique. It's just one of
// many possible ways of setting up client-side routes.
// This module configures crossroads.js, a routing library. If you prefer, you
// can use any other routing library (or none at all) as Knockout is designed to
// compose cleanly with external libraries.
//
// You *don't* have to follow the pattern established here (each route entry
// specifies a 'page', which is a Knockout component) - there's nothing built into
// Knockout that requires or even knows about this technique. It's just one of
// many possible ways of setting up client-side routes.
return new Router({
routes: routes
});
return new Router({
routes: [
{ url: '', params: { page: 'home-page' } }
]
function Router(config) {
var currentRoute = this.currentRoute = ko.observable({});
ko.utils.arrayForEach(config.routes, function(route) {
var addedRoute = crossroads.addRoute(route.url, function(requestParams) {
currentRoute(ko.utils.extend(requestParams, route.params));
});
if (!_.isUndefined(route.rules)) {
addedRoute.rules = route.rules;
}
});
function Router(config) {
var currentRoute = this.currentRoute = ko.observable({});
activateCrossroads();
}
ko.utils.arrayForEach(config.routes, function(route) {
crossroads.addRoute(route.url, function(requestParams) {
currentRoute(ko.utils.extend(requestParams, route.params));
});
});
activateCrossroads();
}
function activateCrossroads() {
function parseHash(newHash, oldHash) { crossroads.parse(newHash); }
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
hasher.initialized.add(parseHash);
hasher.changed.add(parseHash);
hasher.init();
}
function activateCrossroads() {
function parseHash(newHash, oldHash) { crossroads.parse(newHash); }
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
hasher.initialized.add(parseHash);
hasher.changed.add(parseHash);
hasher.init();
}
});

66
src/app/routes.js Normal file
View file

@ -0,0 +1,66 @@
define([], function() {
var YEAR_RE = /^\d{4}$/;
var MONTH_RE = /^\d{2}$/;
var PAGENUMBER_RE = /^\d*$/;
return [
// Page view
{
url: '{slug}',
params: { page: 'post-page', mode: 'page' }
},
// Single post view
{
url: '{year}/{month}/{day}/{slug}',
params: { page: 'post-page', mode: 'post' },
rules: {
year: YEAR_RE,
month: MONTH_RE
}
},
// Year archive
{
url: 'archives/{year}/:pageNumber:',
params: { page: 'year-page', mode: 'year' },
rules: {
year: YEAR_RE,
pageNumber: PAGENUMBER_RE
}
},
// Month archive
{
url: 'archives/{year}/{month}/:pageNumber:',
params: { page: 'month-page', mode: 'month' },
rules: {
year: YEAR_RE,
month: MONTH_RE,
pageNumber: PAGENUMBER_RE
}
},
// Tag archive
{
url: 'tag/{slug}/:pageNumber:',
params: { page: 'tag-page', mode: 'tag' },
rules: {
pageNumber: PAGENUMBER_RE
}
},
// Home view
{
url: ':pageNumber:',
params: { page: 'home-page', mode: 'home' },
rules: {
pageNumber: PAGENUMBER_RE
}
}
];
});

View file

@ -18,6 +18,18 @@ define(['jquery', 'knockout', './router', 'marked', 'bootstrap', 'knockout-proje
ko.components.register('pagination', { require: 'components/pagination/pagination' });
ko.components.register('address-service', { require: 'components/address-service/address-service' });
ko.components.register('tag-page', { require: 'components/tag-page/tag-page' });
ko.components.register('year-page', { require: 'components/year-page/year-page' });
ko.components.register('month-page', { require: 'components/month-page/month-page' });
ko.components.register('post-page', { require: 'components/post-page/post-page' });
ko.components.register('generic-route', { require: 'components/generic-route/generic-route' });
// [Scaffolded component registrations will be inserted here. To retain this feature, don't remove this comment.]
// Set Markdown parser options

View file

@ -0,0 +1,67 @@
define(['knockout', '../../app/routes', '../../app/router', 'hasher'],
function(ko, routes, router, hasher) {
function AddressService(params) {
var self = this;
self.getCurrentRoute = function() {
return router.currentRoute();
};
self.urlTo = function(arguments, mode) {
if (_.isUndefined(mode)) {
mode = self.getCurrentRoute().mode;
// If we are moving to the same mode, use the old arguments as a base to
// extend with new arguments
arguments = _.assign(_.clone(self.getCurrentRoute()), arguments);
}
var route = _.find(routes, function(route) {
return route.params.mode === mode;
});
if (_.isUndefined(route)) {
throw new Error('urlTo given non-existing mode!');
}
var url = route.url
_.forEach(arguments, function(value, key) {
url = url.replace('{' + key + '}', value);
url = url.replace(':' + key + ':', value);
});
return url;
};
self.prefixUrlTo = function(arguments, mode) {
return '/#/' + self.urlTo(arguments, mode);
};
self.goTo = function(arguments, mode) {
var url = self.urlTo(arguments, mode);
hasher.setHash(url);
};
self.postUrl = function(post) {
return self.prefixUrlTo({
year: post.date().format('YYYY'),
month: post.date().format('MM'),
day: post.date().format('DD'),
slug: post.slug()
}, 'post');
};
self.pageUrl = function(page) {
return self.prefixUrlTo({
slug: page.slug()
}, 'page');
}
}
// This runs when the component is torn down. Put here any logic necessary to clean up,
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
AddressService.prototype.dispose = function() {};
return new AddressService();
});

View file

@ -3,6 +3,9 @@ define(['knockout', '../transfer/transfer', 'moment'], function(ko, transfer, mo
function DB(params) {
var self = this;
// Has the index been loaded yet?
self.synced = ko.observable(false);
// List of all posts
self.posts = ko.observableArray();
// List of all pages
@ -35,13 +38,14 @@ define(['knockout', '../transfer/transfer', 'moment'], function(ko, transfer, mo
_.forEach(self.posts(), function(post) {
_.forEach(post().tags(), function(tag) {
if (!(tag in ret)) {
ret[tag] = ko.observableArray(post);
ret[tag] = ko.observableArray([post]);
}
else {
ret[tag].push(post);
}
});
});
return ret;
});
@ -52,12 +56,13 @@ define(['knockout', '../transfer/transfer', 'moment'], function(ko, transfer, mo
var key = post().date().format('YYYY');
if (!(key in ret)) {
ret[key] = ko.observableArray(post);
ret[key] = ko.observableArray([post]);
}
else {
ret[key].push(post);
}
});
return ret;
});
// Dict of months ('YYYY-MM' as key) and the posts for those months
@ -67,12 +72,13 @@ define(['knockout', '../transfer/transfer', 'moment'], function(ko, transfer, mo
var key = post().date().format('YYYY-MM');
if (!(key in ret)) {
ret[key] = ko.observableArray(post);
ret[key] = ko.observableArray([post]);
}
else {
ret[key].push(post);
}
});
return ret;
});
/**
@ -88,7 +94,7 @@ define(['knockout', '../transfer/transfer', 'moment'], function(ko, transfer, mo
};
/**
* Parse data of a single post from a regex match, returning and observable
* Parse data of a single post from a regex match, returning an observable
*/
self.parsePostData = function(matched) {
var tags = {};
@ -113,24 +119,28 @@ define(['knockout', '../transfer/transfer', 'moment'], function(ko, transfer, mo
title: ko.observable(null),
content: ko.observable(null),
isSplit: ko.observable(null),
shortContent: ko.observable(null)
shortContent: ko.observable(null),
isPage: ko.observable(false)
});
return post;
};
/**
* Parse data of a single page from a regex match, returning and observable
* Parse data of a single page from a regex match, returning an observable
*/
self.parsePageData = function(matched) {
var slug = matched[1];
var page = ko.observable({
slug: slug,
linkText: matched[2],
slug: ko.observable(slug),
linkText: ko.observable(matched[2]),
synced: false,
title: null,
isSplit: false
synced: ko.observable(false),
title: ko.observable(null),
content: ko.observable(null),
shortContent: ko.observable(null),
isSplit: ko.observable(false),
isPage: ko.observable(true)
});
return page;
};
@ -191,6 +201,7 @@ define(['knockout', '../transfer/transfer', 'moment'], function(ko, transfer, mo
self.loadIndex = function() {
transfer.loadIndex().success(function(data) {
self.parseIndex(self.posts, self.pages, data);
self.synced(true);
});
};

View file

@ -0,0 +1,25 @@
define(['knockout', '../db/db'], function(ko, DB) {
function GenericRoute() {
var self = this;
self.sync = function() {
if (!DB.synced()) {
DB.loadIndex();
}
};
self.initPage = function(route, page) {
if (!_.isUndefined(route.pageNumber)) {
page(parseInt(route.pageNumber, 10));
}
}
}
// This runs when the component is torn down. Put here any logic necessary to clean up,
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
GenericRoute.prototype.dispose = function() {};
return new GenericRoute();
});

View file

@ -1,3 +1 @@
<h2>Page: <!-- ko text: page --><!-- /ko --></h2>
<pagination params="posts: posts, page: page"></pagination>

View file

@ -1,13 +1,20 @@
define(["knockout", "text!./home.html", "../db/db"], function(ko, homeTemplate, DB) {
define(['knockout', 'text!./home.html', '../generic-route/generic-route', '../db/db'],
function(ko, templateMarkup, GR, DB) {
function HomeViewModel(route) {
function HomePage(route) {
var self = this;
self.posts = DB.posts;
self.page = ko.observable(1);
DB.loadIndex();
self.page = ko.observable(1);
self.posts = DB.posts;
GR.initPage(route, self.page);
GR.sync();
}
return { viewModel: HomeViewModel, template: homeTemplate };
// This runs when the component is torn down. Put here any logic necessary to clean up,
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
HomePage.prototype.dispose = function() {};
return { viewModel: HomePage, template: templateMarkup };
});

View file

@ -0,0 +1,7 @@
<div class="alert alert-info">
<p>
You are viewing the archive for the month <span data-bind="text: month"></span>.
</p>
</div>
<pagination params="posts: posts, page: page"></pagination>

View file

@ -0,0 +1,30 @@
define(['knockout', 'text!./month-page.html', '../generic-route/generic-route', '../db/db'],
function(ko, templateMarkup, GR, DB) {
function MonthPage(route) {
var self = this;
self.month = route.year + '-' + route.month;
self.page = ko.observable(1);
self.posts = ko.pureComputed(function() {
var months = DB.months();
if (self.month in months) {
return months[self.month]();
}
else {
return [];
}
});
GR.initPage(route, self.page);
GR.sync();
}
// This runs when the component is torn down. Put here any logic necessary to clean up,
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
MonthPage.prototype.dispose = function() {};
return { viewModel: MonthPage, template: templateMarkup };
});

View file

@ -1,20 +1,29 @@
<!-- ko if: postsForPage().length === 0 -->
<div class="alert alert-warning">
<p>
<strong>Oops!</strong>
Looks like there's nothing here…
</p>
</div>
<!-- /ko -->
<div class="post-list" data-bind="foreach: { data: postsForPage(), as: 'post' }">
<single-post params="post: post, short: true"></single-post>
</div>
<ul class="pagination" data-bind="if: pageCount() > 1">
<li data-bind="css: { disabled: page() === 1 }">
<a data-bind="click: goBack">
<a data-bind="attr: { href: goBackUrl() }">
&laquo;
</a>
</li>
<!-- ko foreach: _.range(1, pageCount() + 1) -->
<li data-bind="css: { active: $data === $parent.page() } ">
<a data-bind="click: $parent.goTo, text: $data"></a>
<a data-bind="attr: { href: $parent.goToUrl($data) }, text: $data"></a>
</li>
<!-- /ko -->
<li data-bind="css: { disabled: page() === pageCount() }">
<a data-bind="click: goForward">
<a data-bind="attr: { href: goForwardUrl() }">
&raquo;
</a>
</li>

View file

@ -1,4 +1,5 @@
define(['knockout', 'text!./pagination.html'], function(ko, templateMarkup) {
define(['knockout', 'text!./pagination.html', '../address-service/address-service'],
function(ko, templateMarkup, addressService) {
function Pagination(params) {
var self = this;
@ -8,10 +9,10 @@ define(['knockout', 'text!./pagination.html'], function(ko, templateMarkup) {
self.postsPerPage = 3;
self.pageCount = ko.pureComputed(function() {
return Math.ceil(params.posts().length / self.postsPerPage);
return Math.ceil(self.posts().length / self.postsPerPage);
});
self.goTo = function(page) {
self.sanitizePage = function(page) {
if (page < 1) {
page = 1;
}
@ -19,20 +20,25 @@ define(['knockout', 'text!./pagination.html'], function(ko, templateMarkup) {
page = self.pageCount();
}
self.page(page);
return page;
};
self.goBack = function() {
self.goTo(self.page() - 1);
self.goToUrl = function(page) {
page = self.sanitizePage(page);
return addressService.prefixUrlTo({pageNumber: page});
};
self.goBackUrl = function() {
return self.goToUrl(self.page() - 1);
}
self.goForward = function() {
self.goTo(self.page() + 1);
self.goForwardUrl = function() {
return self.goToUrl(self.page() + 1);
}
self.postsForPage = ko.pureComputed(function() {
var start = (self.page() - 1) * self.postsPerPage;
return self.posts.slice(start, start + self.postsPerPage);
return self.posts().slice(start, start + self.postsPerPage);
});
}

View file

@ -0,0 +1,12 @@
<!-- ko if: _.isNull(post()) -->
<div class="alert alert-warning">
<p>
<strong>Oops!</strong>
Looks like there's nothing here…
</p>
</div>
<!-- /ko -->
<!-- ko if: !_.isNull(post()) -->
<single-post params="post: post(), short: false"></single-post>
<!-- /ko -->

View file

@ -0,0 +1,32 @@
define(['knockout', 'text!./post-page.html', '../generic-route/generic-route', '../db/db'],
function(ko, templateMarkup, GR, DB) {
function PostPage(route) {
var self = this;
self.post = ko.pureComputed(function() {
if (route.mode === 'post') {
var posts = DB.postDict();
}
else if (route.mode === 'page') {
var posts = DB.pageDict();
}
if (route.slug in posts) {
return posts[route.slug]();
}
else {
return null;
}
});
GR.sync();
}
// This runs when the component is torn down. Put here any logic necessary to clean up,
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
PostPage.prototype.dispose = function() {};
return { viewModel: PostPage, template: templateMarkup };
});

View file

@ -1,9 +1,37 @@
<div class="single-post">
<h2 data-bind="text: post.title()"></h2>
<!-- ko if: !short -->
<h2 data-bind="text: post.title()">Loading post contents…</h2>
<!-- /ko -->
<p class="post-tags" data-bind="foreach: post.tags()">
<span data-bind="text: $data"></span><!-- ko if: ($index() < post.tags().length - 1) -->,<!-- /ko -->
</p>
<!-- ko if: short -->
<a data-bind="attr: { href: AS.postUrl(post) }">
<h2 data-bind="text: post.title()">Loading post contents…</h2>
</a>
<!-- /ko -->
<div class="post-content" data-bind="html: content() !== null? marked(content()) : ''"></div>
<!-- ko if: !post.isPage() -->
<div class="post-info">
Posted on
<span class="post-date">
<a data-bind="attr: { href: AS.prefixUrlTo({pageNumber: 1, year: year}, 'year') }, text: year"></a><a data-bind="attr: { href: AS.prefixUrlTo({pageNumber: 1, year: year, month: month}, 'month') }, text: month"></a>
</span>
with tags
<span class="post-tags" data-bind="foreach: post.tags()">
<a data-bind="attr: { href: $parent.AS.prefixUrlTo({pageNumber: 1, slug: $data}, 'tag') }, text: $data"></a><!-- ko if: ($index() < $parent.post.tags().length - 1) -->,<!-- /ko --><!-- ko if: ($index() == $parent.post.tags().length - 1) -->.<!-- /ko -->
</span>
</div>
<!-- /ko -->
<div class="post-content-container">
<div class="post-content" data-bind="html: content() !== null? marked(content()) : ''"></div>
<!-- ko if: short && post.isSplit() -->
<a data-bind="attr: { href: AS.postUrl(post) }">
Read more…
</a>
<!-- /ko -->
</div>
</div>

View file

@ -1,10 +1,25 @@
define(['knockout', 'text!./single-post.html', '../db/db', 'marked'],
function(ko, templateMarkup, DB, marked) {
define(['knockout', 'text!./single-post.html', '../db/db', 'marked', '../address-service/address-service'],
function(ko, templateMarkup, DB, marked, addressService) {
function SinglePost(params) {
var self = this;
self.post = params.post;
// For some reason post-page gives params.post as an observable, not an
// object
if (_.isUndefined(params.post.date)) {
self.post = params.post();
}
else {
self.post = params.post;
}
if (!self.post.isPage()) {
self.year = self.post.date().format('YYYY');
self.month = self.post.date().format('MM');
}
self.marked = marked;
self.AS = addressService;
// Is this post shown as short (in pagination) or not?
self.short = params.short;
@ -16,10 +31,12 @@ define(['knockout', 'text!./single-post.html', '../db/db', 'marked'],
// If this post is not fully fetched into the index yet, do that now
if (!self.post.synced()) {
DB.loadPost(self.post.slug());
// Show loading indicator before loading is done
self.post.title("Loading post contents…");
if (!self.post.isPage()) {
DB.loadPost(self.post.slug());
}
else {
DB.loadPage(self.post.slug());
}
}
}

View file

@ -0,0 +1,7 @@
<div class="alert alert-info">
<p>
You are viewing the archive for the tag <span data-bind="text: tag"></span>.
</p>
</div>
<pagination params="posts: posts, page: page"></pagination>

View file

@ -0,0 +1,30 @@
define(['knockout', 'text!./tag-page.html', '../generic-route/generic-route', '../db/db'],
function(ko, templateMarkup, GR, DB) {
function TagPage(route) {
var self = this;
self.tag = route.slug;
self.page = ko.observable(1);
self.posts = ko.pureComputed(function() {
var tags = DB.tags();
if (route.slug in tags) {
return tags[route.slug]();
}
else {
return [];
}
});
GR.initPage(route, self.page);
GR.sync();
}
// This runs when the component is torn down. Put here any logic necessary to clean up,
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
TagPage.prototype.dispose = function() {};
return { viewModel: TagPage, template: templateMarkup };
});

View file

@ -0,0 +1,7 @@
<div class="alert alert-info">
<p>
You are viewing the archive for the year <span data-bind="text: year"></span>.
</p>
</div>
<pagination params="posts: posts, page: page"></pagination>

View file

@ -0,0 +1,30 @@
define(['knockout', 'text!./year-page.html', '../generic-route/generic-route', '../db/db'],
function(ko, templateMarkup, GR, DB) {
function YearPage(route) {
var self = this;
self.year = route.year;
self.page = ko.observable(1);
self.posts = ko.pureComputed(function() {
var years = DB.years();
if (route.year in years) {
return years[route.year]();
}
else {
return [];
}
});
GR.initPage(route, self.page);
GR.sync();
}
// This runs when the component is torn down. Put here any logic necessary to clean up,
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
YearPage.prototype.dispose = function() {};
return { viewModel: YearPage, template: templateMarkup };
});