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',