diff --git a/Gruntfile.js b/Gruntfile.js
index f44b2f07fe..004a11b701 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -375,7 +375,8 @@ var path = require('path'),
'core/client/assets/vendor/countable.js',
'core/client/assets/vendor/to-title-case.js',
'core/client/assets/vendor/packery.pkgd.min.js',
- 'core/client/assets/vendor/fastclick.js'
+ 'core/client/assets/vendor/fastclick.js',
+ 'core/client/assets/vendor/nprogress.js'
],
'core/built/scripts/helpers.js': [
diff --git a/core/client/assets/sass/modules/global.scss b/core/client/assets/sass/modules/global.scss
index 0936b5c10b..c45858296b 100644
--- a/core/client/assets/sass/modules/global.scss
+++ b/core/client/assets/sass/modules/global.scss
@@ -1455,6 +1455,90 @@ main {
}//.pre-image-uploader
+/* ==========================================================================
+ NProgress
+ ========================================================================== */
+
+/* Make clicks pass-through */
+#nprogress {
+ pointer-events: none;
+ -webkit-pointer-events: none;
+}
+
+#nprogress .bar {
+ background: $blue;
+
+ position: fixed;
+ z-index: 100;
+ top: 0;
+ left: 0;
+
+ width: 100%;
+ height: 2px;
+}
+
+/* Fancy blur effect */
+#nprogress .peg {
+ display: block;
+ position: absolute;
+ right: 0px;
+ width: 100px;
+ height: 100%;
+ box-shadow: 0 0 10px $blue, 0 0 5px $blue;
+ opacity: 1.0;
+
+ -webkit-transform: rotate(3deg) translate(0px, -4px);
+ -moz-transform: rotate(3deg) translate(0px, -4px);
+ -ms-transform: rotate(3deg) translate(0px, -4px);
+ -o-transform: rotate(3deg) translate(0px, -4px);
+ transform: rotate(3deg) translate(0px, -4px);
+}
+
+/* Remove these to get rid of the spinner */
+#nprogress .spinner {
+ display: block;
+ position: fixed;
+ z-index: 100;
+ top: 15px;
+ right: 15px;
+}
+
+#nprogress .spinner-icon {
+ width: 14px;
+ height: 14px;
+
+ border: solid 2px transparent;
+ border-top-color: $blue;
+ border-left-color: $blue;
+ border-radius: 10px;
+
+ -webkit-animation: nprogress-spinner 400ms linear infinite;
+ -moz-animation: nprogress-spinner 400ms linear infinite;
+ -ms-animation: nprogress-spinner 400ms linear infinite;
+ -o-animation: nprogress-spinner 400ms linear infinite;
+ animation: nprogress-spinner 400ms linear infinite;
+}
+
+@-webkit-keyframes nprogress-spinner {
+ 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); }
+ 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); }
+}
+@-moz-keyframes nprogress-spinner {
+ 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); }
+ 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); }
+}
+@-o-keyframes nprogress-spinner {
+ 0% { -o-transform: rotate(0deg); transform: rotate(0deg); }
+ 100% { -o-transform: rotate(360deg); transform: rotate(360deg); }
+}
+@-ms-keyframes nprogress-spinner {
+ 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); }
+ 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); }
+}
+@keyframes nprogress-spinner {
+ 0% { transform: rotate(0deg); transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); transform: rotate(360deg); }
+}
/* ==========================================================================
Misc
diff --git a/core/client/assets/vendor/nprogress.js b/core/client/assets/vendor/nprogress.js
new file mode 100644
index 0000000000..f8f0d687c9
--- /dev/null
+++ b/core/client/assets/vendor/nprogress.js
@@ -0,0 +1,275 @@
+/*! NProgress (c) 2013, Rico Sta. Cruz
+ * http://ricostacruz.com/nprogress */
+
+;(function(factory) {
+
+ if (typeof module === 'function') {
+ module.exports = factory(this.jQuery || require('jquery'));
+ } else {
+ this.NProgress = factory(this.jQuery);
+ }
+
+})(function($) {
+ var NProgress = {};
+
+ NProgress.version = '0.1.2';
+
+ var Settings = NProgress.settings = {
+ minimum: 0.08,
+ easing: 'ease',
+ positionUsing: '',
+ speed: 200,
+ trickle: true,
+ trickleRate: 0.02,
+ trickleSpeed: 800,
+ showSpinner: true,
+ template: '
'
+ };
+
+ /**
+ * Updates configuration.
+ *
+ * NProgress.configure({
+ * minimum: 0.1
+ * });
+ */
+ NProgress.configure = function(options) {
+ $.extend(Settings, options);
+ return this;
+ };
+
+ /**
+ * Last number.
+ */
+
+ NProgress.status = null;
+
+ /**
+ * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
+ *
+ * NProgress.set(0.4);
+ * NProgress.set(1.0);
+ */
+
+ NProgress.set = function(n) {
+ var started = NProgress.isStarted();
+
+ n = clamp(n, Settings.minimum, 1);
+ NProgress.status = (n === 1 ? null : n);
+
+ var $progress = NProgress.render(!started),
+ $bar = $progress.find('[role="bar"]'),
+ speed = Settings.speed,
+ ease = Settings.easing;
+
+ $progress[0].offsetWidth; /* Repaint */
+
+ $progress.queue(function(next) {
+ // Set positionUsing if it hasn't already been set
+ if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
+
+ // Add transition
+ $bar.css(barPositionCSS(n, speed, ease));
+
+ if (n === 1) {
+ // Fade out
+ $progress.css({ transition: 'none', opacity: 1 });
+ $progress[0].offsetWidth; /* Repaint */
+
+ setTimeout(function() {
+ $progress.css({ transition: 'all '+speed+'ms linear', opacity: 0 });
+ setTimeout(function() {
+ NProgress.remove();
+ next();
+ }, speed);
+ }, speed);
+ } else {
+ setTimeout(next, speed);
+ }
+ });
+
+ return this;
+ };
+
+ NProgress.isStarted = function() {
+ return typeof NProgress.status === 'number';
+ };
+
+ /**
+ * Shows the progress bar.
+ * This is the same as setting the status to 0%, except that it doesn't go backwards.
+ *
+ * NProgress.start();
+ *
+ */
+ NProgress.start = function() {
+ if (!NProgress.status) NProgress.set(0);
+
+ var work = function() {
+ setTimeout(function() {
+ if (!NProgress.status) return;
+ NProgress.trickle();
+ work();
+ }, Settings.trickleSpeed);
+ };
+
+ if (Settings.trickle) work();
+
+ return this;
+ };
+
+ /**
+ * Hides the progress bar.
+ * This is the *sort of* the same as setting the status to 100%, with the
+ * difference being `done()` makes some placebo effect of some realistic motion.
+ *
+ * NProgress.done();
+ *
+ * If `true` is passed, it will show the progress bar even if its hidden.
+ *
+ * NProgress.done(true);
+ */
+
+ NProgress.done = function(force) {
+ if (!force && !NProgress.status) return this;
+
+ return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
+ };
+
+ /**
+ * Increments by a random amount.
+ */
+
+ NProgress.inc = function(amount) {
+ var n = NProgress.status;
+
+ if (!n) {
+ return NProgress.start();
+ } else {
+ if (typeof amount !== 'number') {
+ amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95);
+ }
+
+ n = clamp(n + amount, 0, 0.994);
+ return NProgress.set(n);
+ }
+ };
+
+ NProgress.trickle = function() {
+ return NProgress.inc(Math.random() * Settings.trickleRate);
+ };
+
+ /**
+ * (Internal) renders the progress bar markup based on the `template`
+ * setting.
+ */
+
+ NProgress.render = function(fromStart) {
+ if (NProgress.isRendered()) return $("#nprogress");
+ $('html').addClass('nprogress-busy');
+
+ var $el = $("")
+ .html(Settings.template);
+
+ var perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0);
+
+ $el.find('[role="bar"]').css({
+ transition: 'all 0 linear',
+ transform: 'translate3d('+perc+'%,0,0)'
+ });
+
+ if (!Settings.showSpinner)
+ $el.find('[role="spinner"]').remove();
+
+ $el.appendTo(document.body);
+
+ return $el;
+ };
+
+ /**
+ * Removes the element. Opposite of render().
+ */
+
+ NProgress.remove = function() {
+ $('html').removeClass('nprogress-busy');
+ $('#nprogress').remove();
+ };
+
+ /**
+ * Checks if the progress bar is rendered.
+ */
+
+ NProgress.isRendered = function() {
+ return ($("#nprogress").length > 0);
+ };
+
+ /**
+ * Determine which positioning CSS rule to use.
+ */
+
+ NProgress.getPositioningCSS = function() {
+ // Sniff on document.body.style
+ var bodyStyle = document.body.style;
+
+ // Sniff prefixes
+ var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
+ ('MozTransform' in bodyStyle) ? 'Moz' :
+ ('msTransform' in bodyStyle) ? 'ms' :
+ ('OTransform' in bodyStyle) ? 'O' : '';
+
+ if (vendorPrefix + 'Perspective' in bodyStyle) {
+ // Modern browsers with 3D support, e.g. Webkit, IE10
+ return 'translate3d';
+ } else if (vendorPrefix + 'Transform' in bodyStyle) {
+ // Browsers without 3D support, e.g. IE9
+ return 'translate';
+ } else {
+ // Browsers without translate() support, e.g. IE7-8
+ return 'margin';
+ }
+ };
+
+ /**
+ * Helpers
+ */
+
+ function clamp(n, min, max) {
+ if (n < min) return min;
+ if (n > max) return max;
+ return n;
+ }
+
+ /**
+ * (Internal) converts a percentage (`0..1`) to a bar translateX
+ * percentage (`-100%..0%`).
+ */
+
+ function toBarPerc(n) {
+ return (-1 + n) * 100;
+ }
+
+
+ /**
+ * (Internal) returns the correct CSS for changing the bar's
+ * position given an n percentage, and speed and ease from Settings
+ */
+
+ function barPositionCSS(n, speed, ease) {
+ var barCSS;
+
+ if (Settings.positionUsing === 'translate3d') {
+ barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
+ } else if (Settings.positionUsing === 'translate') {
+ barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' };
+ } else {
+ barCSS = { 'margin-left': toBarPerc(n)+'%' };
+ }
+
+ barCSS.transition = 'all '+speed+'ms '+ease;
+
+ return barCSS;
+ }
+
+ return NProgress;
+});
+
diff --git a/core/client/models/base.js b/core/client/models/base.js
new file mode 100644
index 0000000000..2c523581de
--- /dev/null
+++ b/core/client/models/base.js
@@ -0,0 +1,23 @@
+/*global window, document, setTimeout, Ghost, $, _, Backbone, JST, shortcut, NProgress */
+
+(function () {
+ "use strict";
+ NProgress.configure({ showSpinner: false });
+
+ Ghost.TemplateModel = Backbone.Model.extend({
+
+ // Adds in a call to start a loading bar
+ // This is sets up a success function which completes the loading bar
+ fetch : function (options) {
+ options = options || {};
+
+ NProgress.start();
+
+ options.success = function () {
+ NProgress.done();
+ };
+
+ return Backbone.Collection.prototype.fetch.call(this, options);
+ }
+ });
+}());
\ No newline at end of file
diff --git a/core/client/models/post.js b/core/client/models/post.js
index 360a3420ff..8e5c72feec 100644
--- a/core/client/models/post.js
+++ b/core/client/models/post.js
@@ -2,7 +2,7 @@
(function () {
'use strict';
- Ghost.Models.Post = Backbone.Model.extend({
+ Ghost.Models.Post = Ghost.TemplateModel.extend({
defaults: {
status: 'draft'
diff --git a/core/client/models/settings.js b/core/client/models/settings.js
index 1d0e9a38b7..6d7ff2fd8d 100644
--- a/core/client/models/settings.js
+++ b/core/client/models/settings.js
@@ -2,7 +2,7 @@
(function () {
'use strict';
//id:0 is used to issue PUT requests
- Ghost.Models.Settings = Backbone.Model.extend({
+ Ghost.Models.Settings = Ghost.TemplateModel.extend({
url: Ghost.settings.apiRoot + '/settings/?type=blog,theme',
id: '0'
});
diff --git a/core/client/models/tag.js b/core/client/models/tag.js
index 444e3526d4..70cd9020e2 100644
--- a/core/client/models/tag.js
+++ b/core/client/models/tag.js
@@ -2,7 +2,7 @@
(function () {
'use strict';
- Ghost.Collections.Tags = Backbone.Collection.extend({
+ Ghost.Collections.Tags = Ghost.TemplateModel.extend({
url: Ghost.settings.apiRoot + '/tags/'
});
}());
diff --git a/core/client/models/themes.js b/core/client/models/themes.js
index 5e82d77201..03ebc3ecad 100644
--- a/core/client/models/themes.js
+++ b/core/client/models/themes.js
@@ -2,7 +2,7 @@
(function () {
'use strict';
- Ghost.Models.Themes = Backbone.Model.extend({
+ Ghost.Models.Themes = Ghost.TemplateModel.extend({
url: Ghost.settings.apiRoot + '/themes'
});
diff --git a/core/client/models/uploadModal.js b/core/client/models/uploadModal.js
index 68dff9ba6e..dadbba2a21 100644
--- a/core/client/models/uploadModal.js
+++ b/core/client/models/uploadModal.js
@@ -1,7 +1,7 @@
/*global Ghost, Backbone, $ */
(function () {
'use strict';
- Ghost.Models.uploadModal = Backbone.Model.extend({
+ Ghost.Models.uploadModal = Ghost.TemplateModel.extend({
options: {
close: true,
diff --git a/core/client/models/user.js b/core/client/models/user.js
index 7f30a4f895..4ad165a8f3 100644
--- a/core/client/models/user.js
+++ b/core/client/models/user.js
@@ -2,7 +2,7 @@
(function () {
'use strict';
- Ghost.Models.User = Backbone.Model.extend({
+ Ghost.Models.User = Ghost.TemplateModel.extend({
url: Ghost.settings.apiRoot + '/users/me/'
});
diff --git a/core/client/models/widget.js b/core/client/models/widget.js
index 53c0977f31..23306e5abc 100644
--- a/core/client/models/widget.js
+++ b/core/client/models/widget.js
@@ -2,7 +2,7 @@
(function () {
'use strict';
- Ghost.Models.Widget = Backbone.Model.extend({
+ Ghost.Models.Widget = Ghost.TemplateModel.extend({
defaults: {
title: '',
diff --git a/core/client/router.js b/core/client/router.js
index b9dbe34255..0db5937052 100644
--- a/core/client/router.js
+++ b/core/client/router.js
@@ -1,4 +1,4 @@
-/*global window, document, Ghost, Backbone, $, _ */
+/*global window, document, Ghost, Backbone, $, _, NProgress */
(function () {
"use strict";
@@ -30,8 +30,10 @@
blog: function () {
var posts = new Ghost.Collections.Posts();
+ NProgress.start();
posts.fetch({ data: { status: 'all', orderBy: ['updated_at', 'DESC'] } }).then(function () {
Ghost.currentView = new Ghost.Views.Blog({ el: '#main', collection: posts });
+ NProgress.done();
});
},
diff --git a/core/client/views/blog.js b/core/client/views/blog.js
index 24a1c7bd0f..9818ce2ae9 100644
--- a/core/client/views/blog.js
+++ b/core/client/views/blog.js
@@ -1,4 +1,4 @@
-/*global window, document, Ghost, $, _, Backbone, JST */
+/*global window, document, Ghost, $, _, Backbone, JST, NProgress */
(function () {
"use strict";
@@ -10,6 +10,12 @@
// ----------
Ghost.Views.Blog = Ghost.View.extend({
initialize: function (options) {
+ this.listenTo(this.collection, 'request', function () {
+ NProgress.start();
+ });
+ this.listenTo(this.collection, 'sync', function () {
+ NProgress.done();
+ });
this.addSubview(new PreviewContainer({ el: '.js-content-preview', collection: this.collection })).render();
this.addSubview(new ContentList({ el: '.js-content-list', collection: this.collection })).render();
}
@@ -80,7 +86,6 @@
// Load moar posts!
this.isLoading = true;
-
this.collection.fetch({
data: {
status: 'all',