diff --git a/content/themes/casper/assets/css/screen.css b/content/themes/casper/assets/css/screen.css index dbf77431..120783f3 100644 --- a/content/themes/casper/assets/css/screen.css +++ b/content/themes/casper/assets/css/screen.css @@ -184,6 +184,7 @@ body { -webkit-font-feature-settings: 'kern' 1; -moz-font-feature-settings: 'kern' 1; -o-font-feature-settings: 'kern' 1; + text-rendering: geometricPrecision; } ::-moz-selection { @@ -203,6 +204,7 @@ h4, h5, h6 { line-height: 1.15em; margin: 0 0 0.4em 0; font-family: "Open Sans", sans-serif; + text-rendering: geometricPrecision; } h1 { @@ -234,7 +236,7 @@ h6 { a { color: #4A4A4A; - transition: color ease 0.3s; + transition: color 0.3s ease; } a:hover { @@ -246,6 +248,7 @@ p, ul, ol, dl { -moz-font-feature-settings: 'liga' 1, 'onum' 1, 'kern' 1; -o-font-feature-settings: 'liga' 1, 'onum' 1, 'kern' 1; margin: 0 0 1.75em 0; + text-rendering: geometricPrecision; } ol, ul { @@ -351,7 +354,6 @@ pre { pre code, tt { font-size: inherit; - white-space: -moz-pre-wrap; white-space: pre-wrap; background: transparent; border: none; @@ -440,7 +442,7 @@ margin on the iframe, cause it breaks stuff. */ display: table; } .clearfix:after { clear: both; } -.clearfix { *zoom: 1; } +.clearfix { zoom: 1; } /* Hides shit */ .hidden { @@ -463,6 +465,22 @@ margin on the iframe, cause it breaks stuff. */ vertical-align: middle; } +/* Wraps the main content & footer */ +.site-wrapper { + position: relative; + z-index: 10; + min-height: 100%; + background: #fff; + -webkit-transition: -webkit-transform 0.5s ease; + transition: transform 0.5s ease; +} + +body.nav-opened .site-wrapper { + -webkit-transform: translate3D(-240px, 0, 0); + -ms-transform: translate3D(-240px, 0, 0); + transform: translate3D(-240px, 0, 0); +} + /* ========================================================================== 4. General - The main styles for the the theme @@ -473,7 +491,7 @@ margin on the iframe, cause it breaks stuff. */ position: relative; display: table; width: 100%; - height: 100%; + height: 100vh; margin-bottom: 5rem; text-align: center; background: #222 no-repeat center center; @@ -496,6 +514,192 @@ margin on the iframe, cause it breaks stuff. */ font-family: 'Open Sans', sans-serif; } +/* Navigation */ +body.nav-opened .nav-cover { + position: fixed; + top: 0; + left: 0; + right: 240px; + bottom: 0; + z-index: 200; +} + +.nav { + position: fixed; + top: 0; + right: 0; + bottom: 0; + z-index: 5; + width: 240px; + opacity: 0; + background: #111; + margin-bottom: 0; + text-align: left; + overflow-y: auto; + -webkit-transition: -webkit-transform 0.5s ease, + opacity 0.3s ease 0.7s; + transition: transform 0.5s ease, + opacity 0.3s ease 0.7s; +} + +body.nav-closed .nav { + -webkit-transform: translate3D(97px, 0, 0); + -ms-transform: translate3D(97px, 0, 0); + transform: translate3D(97px, 0, 0); +} + +body.nav-opened .nav { + opacity: 1; + -webkit-transition: -webkit-transform 0.5s ease, + opacity 0s ease 0s; + transition: -webkit-transform 0.5s ease, + opacity 0s ease 0s; + -webkit-transform: translate3D(0, 0, 0); + -ms-transform: translate3D(0, 0, 0); + transform: translate3D(0, 0, 0); +} + +.nav-title { + position: absolute; + top: 45px; + left: 30px; + font-size: 16px; + font-weight: 100; + text-transform: uppercase; + color: #fff; +} + +.nav-close { + position: absolute; + top: 38px; + right: 25px; + width: 20px; + height: 20px; + padding: 0; + font-size: 10px; +} + +.nav-close:focus { + outline: 0; +} + +.nav-close:before, +.nav-close:after { + content: ''; + position: absolute; + top: 0; + width: 20px; + height: 1px; + background: rgb(150,150,150); + top: 15px; + -webkit-transition: background 0.15s ease; + transition: background 0.15s ease; +} + +.nav-close:before { + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +.nav-close:after { + -webkit-transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + transform: rotate(-45deg); +} + +.nav-close:hover:before, +.nav-close:hover:after { + background: rgb(255,255,255); +} + +.nav ul { + padding: 90px 9% 5%; + list-style: none; + counter-reset: item; +} + +.nav li:before { + display: block; + float: right; + padding-right: 4%; + padding-left: 5px; + text-align: right; + font-size: 1.2rem; + vertical-align: bottom; + color: #B8B8B8; + content: counter(item, lower-roman); + counter-increment: item; +} +.nav li { + margin: 0; +} +.nav li a { + text-decoration: none; + line-height: 1.4; + font-size: 1.4rem; + display: block; + padding: 0.6rem 4%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.nav li a:after { + display: inline-block; + content: " ......................................................."; + color: rgba(255,255,255,0.2); + margin-left: 5px; +} +.nav .nav-current:before { + color: #fff; +} +.nav .nav-current a:after { + content: " "; + border-bottom: rgba(255,255,255,0.5) 1px solid; + width: 100%; + height: 1px; +} + +.nav a:link, +.nav a:visited { + color: #B8B8B8; +} + +.nav li.nav-current a, +.nav a:hover, +.nav a:active, +.nav a:focus { + color: #fff; +} + +.subscribe-button { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + display: block; + position: absolute; + bottom: 30px; + left: 30px; + right: 30px; + height: 38px; + padding: 0 20px; + color: #111 !important; + text-align: center; + font-size: 12px; + font-family: "Open Sans", sans-serif; + text-transform: uppercase; + text-decoration: none; + line-height: 35px; + border-radius: 3px; + background: #fff; + transition: all ease 0.3s; +} +.subscribe-button:before { + font-size: 9px; + margin-right: 6px; +} + + /* Create a bouncing scroll-down arrow on homepage with cover image */ .scroll-down { display: block; @@ -511,6 +715,7 @@ margin on the iframe, cause it breaks stuff. */ text-decoration: none; color: rgba(255,255,255,0.7); -webkit-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); transform: rotate(-90deg); -webkit-animation: bounce 4s 2s infinite; animation: bounce 4s 2s infinite; @@ -535,9 +740,6 @@ margin on the iframe, cause it breaks stuff. */ bottom: 0; left: 50%; margin-left: -75px; - background: -moz-radial-gradient(center, ellipse cover, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0) 70%, rgba(0,0,0,0) 100%); - background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(0,0,0,0.15)), color-stop(70%,rgba(0,0,0,0)), color-stop(100%,rgba(0,0,0,0))); - background: -webkit-radial-gradient(center, ellipse cover, rgba(0,0,0,0.15) 0%,rgba(0,0,0,0) 70%,rgba(0,0,0,0) 100%); background: radial-gradient(ellipse at center, rgba(0,0,0,0.15) 0%,rgba(0,0,0,0) 70%,rgba(0,0,0,0) 100%); } @@ -567,41 +769,16 @@ margin on the iframe, cause it breaks stuff. */ width: auto; } -.back-button { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - display: inline-block; - float: left; - height: 38px; - padding: 0 15px 0 10px; - border: transparent 1px solid; - color: #9EABB3; - text-align: center; - font-size: 12px; - text-transform: uppercase; - line-height: 35px; - border-radius: 3px; - background: rgba(0,0,0,0.1); - transition: all ease 0.3s; -} -.back-button:before { - position: relative; - bottom: -2px; - font-size: 13px; - line-height: 0; - margin-right: 8px; -} - -.subscribe-button { +.menu-button { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; display: inline-block; float: right; height: 38px; - padding: 0 20px; - border: transparent 1px solid; + padding: 0 15px; + border: #bfc8cd 1px solid; + opacity: 1; color: #9EABB3; text-align: center; font-size: 12px; @@ -610,13 +787,31 @@ margin on the iframe, cause it breaks stuff. */ white-space: nowrap; border-radius: 3px; background: rgba(0,0,0,0.1); - transition: all ease 0.3s; + transition: all 0.6s ease; } -.subscribe-button:before { - font-size: 9px; +.menu-button:focus { + outline: 0; +} +.menu-button .burger { + font-size: 12px; margin-right: 6px; } +body.nav-opened .menu-button { + padding: 0 12px; + background: #111 !important; + border-color: #111 !important; + color: #fff !important; + -webkit-transform: translate3D(94px, 0, 0); + -ms-transform: translate3D(94px, 0, 0); + transform: translate3D(94px, 0, 0); + transition: all 0.4s ease; +} +body.nav-opened .menu-button .word { + opacity: 0; + transition: all 0.3s ease; +} + /* Special styles when overlaid on an image*/ .main-nav.overlay { position: absolute; @@ -625,14 +820,10 @@ margin on the iframe, cause it breaks stuff. */ right: 0; height: 70px; border: none; - background: -moz-linear-gradient(top, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0) 100%); - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(0,0,0,0.2)), color-stop(100%,rgba(0,0,0,0))); - background: -webkit-linear-gradient(top, rgba(0,0,0,0.2) 0%,rgba(0,0,0,0) 100%); background: linear-gradient(to bottom, rgba(0,0,0,0.2) 0%,rgba(0,0,0,0) 100%); } .no-cover .main-nav.overlay, -.no-cover .back-button, -.no-cover .subscribe-button { +.no-cover .menu-button { background: none; } @@ -640,8 +831,7 @@ margin on the iframe, cause it breaks stuff. */ color: #fff; } -.main-nav.overlay .back-button, -.main-nav.overlay .subscribe-button { +.main-nav.overlay .menu-button { border-color: rgba(255,255,255,0.6); } @@ -649,14 +839,12 @@ margin on the iframe, cause it breaks stuff. */ color: #222; border-color: #fff; background: #fff; - transition: all 0.1s ease; } /* Add a border to the buttons on hover */ -.back-button:hover, -.subscribe-button:hover { - border-color: #bfc8cd; - color: #9EABB3; +.menu-button:hover { + border-color: #555; + color: #555; } /* The details of your blog. Defined in ghost/settings/ */ @@ -693,8 +881,7 @@ margin on the iframe, cause it breaks stuff. */ color: rgba(0,0,0,0.5); } -.no-cover .main-nav.overlay .back-button, -.no-cover .main-nav.overlay .subscribe-button { +.no-cover .main-nav.overlay .menu-button { color: rgba(0,0,0,0.4); border-color: rgba(0,0,0,0.3); } @@ -721,7 +908,10 @@ margin on the iframe, cause it breaks stuff. */ margin: 4rem auto; padding-bottom: 4rem; border-bottom: #EBF2F6 1px solid; - word-break: break-word; + word-wrap: break-word; + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; hyphens: auto; } @@ -738,8 +928,6 @@ margin on the iframe, cause it breaks stuff. */ left: 50%; margin-left: -5px; background: #FFF; - -webkit-border-radius: 100%; - -moz-border-radius: 100%; border-radius: 100%; box-shadow: #FFF 0 0 0 5px; } @@ -973,89 +1161,13 @@ body:not(.post-template) .post-title { color: #50585D; } -/* The subscribe icon on the footer */ -.subscribe { - width: 28px; - height: 28px; - position: absolute; - top: -14px; - left: 50%; - margin-left: -15px; - border: #EBF2F6 1px solid; - text-align: center; - line-height: 2.4rem; - border-radius: 50px; - background: #FFF; - transition: box-shadow 0.5s; -} - -/* The RSS icon, inserted via icon font */ -.subscribe:before { - color: #D2DEE3; - font-size: 10px; - position: absolute; - top: 2px; - left: 9px; - font-weight: 700; - transition: color 0.5s ease; -} - -/* Add a box shadow to on hover */ -.subscribe:hover { - box-shadow: rgba(0,0,0,0.05) 0 0 0 3px; - transition: box-shadow 0.25s; -} - -.subscribe:hover:before { - color: #50585D; -} - -/* CSS tooltip saying "Subscribe!" - initially hidden */ -.tooltip { - opacity: 0; - display: block; - width: 53px; - padding: 4px 8px 5px 8px; - position:absolute; - top: -23px; - left: -21px; - color: rgba(255,255,255,0.9); - font-size: 1.1rem; - line-height: 1em; - text-align: center; - background: #50585D; - border-radius: 20px; - box-shadow: 0 1px 4px rgba(0,0,0,0.1); - transition: opacity 0.3s ease, top 0.3s ease; -} - -/* The little chiclet arrow under the tooltip, pointing down */ -.tooltip:after { - content: " "; - border-width: 5px 5px 0 5px; - border-style: solid; - border-color: #50585D transparent; - display: block; - position: absolute; - bottom: -4px; - left: 50%; - margin-left: -5px; - z-index: 220; - width: 0; -} - -/* On hover, show the tooltip! */ -.subscribe:hover .tooltip { - opacity: 1; - top: -33px; -} /* ========================================================================== 6. Author profile ========================================================================== */ .post-head.main-header { - height: 65%; + height: 65vh; min-height: 180px; } @@ -1067,17 +1179,17 @@ body:not(.post-template) .post-title { } .tag-head.main-header { - height: 40%; + height: 40vh; min-height: 180px; } .author-head.main-header { - height: 40%; + height: 40vh; min-height: 180px; } .no-cover.author-head.main-header { - height: 10%; + height: 10vh; min-height: 100px; background: transparent; } @@ -1100,8 +1212,6 @@ body:not(.post-template) .post-title { left: 50%; margin-left: -5px; background: #FFF; - -webkit-border-radius: 100%; - -moz-border-radius: 100%; border-radius: 100%; box-shadow: #FFF 0 0 0 5px; } @@ -1193,9 +1303,11 @@ body:not(.post-template) .post-title { margin: 0; font-size: 1.4rem; } +.gist td { + line-height: 1.4; +} .gist .line-number { min-width: 25px; - font-size: 1.1rem; } /* Pastebin */ @@ -1233,7 +1345,7 @@ body:not(.post-template) .post-title { border: #bfc8cd 1px solid; text-decoration: none; border-radius: 4px; - transition: border ease 0.3s; + transition: border 0.3s ease; } .older-posts { @@ -1271,8 +1383,6 @@ body:not(.post-template) .post-title { left: 50%; margin-left: -5px; background: #FFF; - -webkit-border-radius: 100%; - -moz-border-radius: 100%; border-radius: 100%; box-shadow: #FFF 0 0 0 5px; } @@ -1336,10 +1446,6 @@ body:not(.post-template) .post-title { @media only screen and (max-width: 900px) { - .main-nav { - padding: 15px; - } - blockquote { margin-left: 0; } @@ -1476,40 +1582,33 @@ body:not(.post-template) .post-title { } .main-nav { - padding: 0; + padding: 5px; margin-bottom: 2rem; - border-bottom: #e0e4e7 1px solid; } .blog-logo { - padding: 10px 10px; + padding: 5px; } .blog-logo img { - height: 26px; + height: 30px; } - .back-button, - .subscribe-button { - height: 44px; - line-height: 41px; + .menu-button { + padding: 0 5px; border-radius: 0; + border-color: transparent; color: #2e2e2e; background: transparent; } - .back-button:hover, - .subscribe-button:hover { - border-color: #ebeef0; + .menu-button:hover { color: #2e2e2e; - background: #ebeef0; - } - - .back-button { - padding: 0 15px 0 10px; + border-color: transparent; + background: none; } - - .subscribe-button { - padding: 0 12px; + body.nav-opened .menu-button { + background: none !important; + border: transparent !important; } .main-nav.overlay a:hover { @@ -1521,18 +1620,26 @@ body:not(.post-template) .post-title { .no-cover .main-nav.overlay { background: none; } - .no-cover .main-nav.overlay .back-button, - .no-cover .main-nav.overlay .subscribe-button { + .no-cover .main-nav.overlay .menu-button { border: none; } - .main-nav.overlay .back-button, - .main-nav.overlay .subscribe-button { + .main-nav.overlay .menu-button { border-color: transparent; } - .blog-logo img { - max-height: 80px; + .nav-title { + top: 25px; + + } + + .nav-close { + position: absolute; + top: 18px; + } + + .nav ul { + padding: 60px 9% 5%; } .inner, diff --git a/content/themes/casper/assets/js/index.js b/content/themes/casper/assets/js/index.js index 64b01914..9fc485ba 100644 --- a/content/themes/casper/assets/js/index.js +++ b/content/themes/casper/assets/js/index.js @@ -59,6 +59,11 @@ $(".scroll-down").arctic_scroll(); + $(".menu-button, .nav-cover, .nav-close").on("click", function(e){ + e.preventDefault(); + $("body").toggleClass("nav-opened nav-closed"); + }); + }); // smartresize diff --git a/content/themes/casper/author.hbs b/content/themes/casper/author.hbs index b94cdfb5..0da102bc 100644 --- a/content/themes/casper/author.hbs +++ b/content/themes/casper/author.hbs @@ -7,8 +7,10 @@ {{#author}}
@@ -19,7 +21,9 @@ {{/if}}

{{name}}

-

{{bio}}

+ {{#if bio}} +

{{bio}}

+ {{/if}}
{{#if location}}{{location}}{{/if}} {{#if website}}{{website}}{{/if}} @@ -34,4 +38,4 @@ {{! The tag below includes the post loop - partials/loop.hbs }} {{> "loop"}} - \ No newline at end of file + diff --git a/content/themes/casper/default.hbs b/content/themes/casper/default.hbs index 2a1fea81..a04ca4c8 100644 --- a/content/themes/casper/default.hbs +++ b/content/themes/casper/default.hbs @@ -21,15 +21,21 @@ {{! Ghost outputs important style and meta data with this tag }} {{ghost_head}} - + - {{! Everything else gets inserted here }} - {{{body}}} + {{navigation}} - +
+ + {{! Everything else gets inserted here }} + {{{body}}} + + + +
{{! Ghost outputs important scripts and data with this tag }} {{ghost_foot}} diff --git a/content/themes/casper/index.hbs b/content/themes/casper/index.hbs index 80cf642d..223f9eda 100644 --- a/content/themes/casper/index.hbs +++ b/content/themes/casper/index.hbs @@ -5,7 +5,9 @@
@@ -22,4 +24,4 @@ {{! The tag below includes the post loop - partials/loop.hbs }} {{> "loop"}} - \ No newline at end of file + diff --git a/content/themes/casper/package.json b/content/themes/casper/package.json index 509eb5e0..8076cb59 100644 --- a/content/themes/casper/package.json +++ b/content/themes/casper/package.json @@ -1,4 +1,4 @@ { "name": "Casper", - "version": "1.1.5" + "version": "1.1.6" } diff --git a/content/themes/casper/page.hbs b/content/themes/casper/page.hbs index b1c5f520..e77a0bdb 100644 --- a/content/themes/casper/page.hbs +++ b/content/themes/casper/page.hbs @@ -8,15 +8,15 @@
-
- {{! Everything inside the #post tags pulls data from the post }}

{{title}}

@@ -27,6 +27,5 @@
-
{{/post}} diff --git a/content/themes/casper/partials/navigation.hbs b/content/themes/casper/partials/navigation.hbs new file mode 100644 index 00000000..c97d4c2c --- /dev/null +++ b/content/themes/casper/partials/navigation.hbs @@ -0,0 +1,13 @@ + + diff --git a/content/themes/casper/post.hbs b/content/themes/casper/post.hbs index 273bd161..cd80335f 100644 --- a/content/themes/casper/post.hbs +++ b/content/themes/casper/post.hbs @@ -8,13 +8,14 @@
-
@@ -86,7 +87,6 @@
-
{{/post}} diff --git a/content/themes/casper/tag.hbs b/content/themes/casper/tag.hbs index 56ef106c..cebbab80 100644 --- a/content/themes/casper/tag.hbs +++ b/content/themes/casper/tag.hbs @@ -4,8 +4,10 @@ {{! If we have a tag cover, display that - else blog cover - else nothing }}
diff --git a/node_modules/ghost/Gruntfile.js b/node_modules/ghost/Gruntfile.js index b29596a0..3f87aa47 100644 --- a/node_modules/ghost/Gruntfile.js +++ b/node_modules/ghost/Gruntfile.js @@ -339,6 +339,10 @@ var _ = require('lodash'), coverage: { command: 'node ' + mochaPath + ' --timeout 15000 --reporter html-cov > coverage.html ' + path.resolve(cwd + '/core/test/blanket_coverage.js') + }, + + shrinkwrap: { + command: 'npm shrinkwrap' } }, @@ -347,7 +351,7 @@ var _ = require('lodash'), sass: { compress: { options: { - outputStyle: 'nested', // TODO: Set back to 'compressed' working correctly with our dependencies + outputStyle: 'compressed', sourceMap: true }, files: [ @@ -380,6 +384,9 @@ var _ = require('lodash'), emberTemplates: { dev: { options: { + templateCompilerPath: 'bower_components/ember/ember-template-compiler.js', + handlebarsPath: 'bower_components/handlebars/handlebars.js', + templateNamespace: 'HTMLBars', templateBasePath: /core\/client\//, templateFileExtensions: /\.hbs/, templateRegistration: function (name, template) { @@ -395,6 +402,9 @@ var _ = require('lodash'), prod: { options: { + templateCompilerPath: 'bower_components/ember/ember-template-compiler.js', + handlebarsPath: 'bower_components/handlebars/handlebars.js', + templateNamespace: 'HTMLBars', templateBasePath: /core\/client\//, templateFileExtensions: /\.hbs/, templateRegistration: function (name, template) { @@ -571,8 +581,7 @@ var _ = require('lodash'), src: [ 'bower_components/loader.js/loader.js', 'bower_components/jquery/dist/jquery.js', - 'bower_components/handlebars/handlebars.js', - 'bower_components/ember/ember.js', + 'bower_components/ember/ember.debug.js', 'bower_components/ember-data/ember-data.js', 'bower_components/ember-resolver/dist/ember-resolver.js', 'bower_components/ic-ajax/dist/globals/main.js', @@ -594,6 +603,7 @@ var _ = require('lodash'), 'bower_components/ember-simple-auth/simple-auth-oauth2.js', 'bower_components/google-caja/html-css-sanitizer-bundle.js', 'bower_components/nanoscroller/bin/javascripts/jquery.nanoscroller.js', + 'bower_components/jqueryui-touch-punch/jquery.ui.touch-punch.js', 'core/shared/lib/showdown/extensions/ghostimagepreview.js', 'core/shared/lib/showdown/extensions/ghostgfm.js', @@ -608,7 +618,6 @@ var _ = require('lodash'), src: [ 'bower_components/loader.js/loader.js', 'bower_components/jquery/dist/jquery.js', - 'bower_components/handlebars/handlebars.runtime.js', 'bower_components/ember/ember.prod.js', 'bower_components/ember-data/ember-data.prod.js', 'bower_components/ember-resolver/dist/ember-resolver.js', @@ -631,6 +640,7 @@ var _ = require('lodash'), 'bower_components/ember-simple-auth/simple-auth-oauth2.js', 'bower_components/google-caja/html-css-sanitizer-bundle.js', 'bower_components/nanoscroller/bin/javascripts/jquery.nanoscroller.js', + 'bower_components/jqueryui-touch-punch/jquery.ui.touch-punch.js', 'core/shared/lib/showdown/extensions/ghostimagepreview.js', 'core/shared/lib/showdown/extensions/ghostgfm.js', @@ -810,7 +820,7 @@ var _ = require('lodash'), grunt.log.write('no test provided'); } - grunt.task.run('setTestEnv', 'shell:test:' + test); + grunt.task.run('test-setup', 'shell:test:' + test); }); // ### Validate @@ -825,21 +835,30 @@ var _ = require('lodash'), grunt.registerTask('validate', 'Run tests and lint code', ['init', 'test-all']); - // ### Test + // ### Test-All // **Main testing task** // - // `grunt test` will lint and test your pre-built local Ghost codebase. + // `grunt test-all` will lint and test your pre-built local Ghost codebase. // - // `grunt test` runs jshint and jscs as well as the 4 test suites. See the individual sub tasks below for + // `grunt test-all` runs jshint and jscs as well as all 6 test suites. See the individual sub tasks below for // details of each of the test suites. // grunt.registerTask('test-all', 'Run tests and lint code', - ['jshint', 'jscs', 'test-routes', 'test-module', 'test-unit', 'test-integration', 'test-functional', 'shell:testem']); + ['lint', 'test-routes', 'test-module', 'test-unit', 'test-integration', 'test-functional', 'shell:testem'] + ); // ### Lint // // `grunt lint` will run the linter and the code style checker so you can make sure your code is pretty - grunt.registerTask('lint', 'Run the code style checks and linter', ['jshint', 'jscs']); + grunt.registerTask('lint', 'Run the code style checks and linter', + ['jshint', 'jscs'] + ); + + // ### test-setup *(utility)( + // `grunt test-setup` will run all the setup tasks required for running tests + grunt.registerTask('test-setup', 'Setup ready to run tests', + ['clean:test', 'setTestEnv', 'ensureConfig'] + ); // ### Unit Tests *(sub task)* // `grunt test-unit` will run just the unit tests @@ -859,7 +878,8 @@ var _ = require('lodash'), // Unit tests do **not** touch the database. // A coverage report can be generated for these tests using the `grunt test-coverage` task. grunt.registerTask('test-unit', 'Run unit tests (mocha)', - ['clean:test', 'setTestEnv', 'ensureConfig', 'mochacli:unit']); + ['test-setup', 'mochacli:unit'] + ); // ### Integration tests *(sub task)* // `grunt test-integration` will run just the integration tests @@ -888,7 +908,8 @@ var _ = require('lodash'), // // A coverage report can be generated for these tests using the `grunt test-coverage` task. grunt.registerTask('test-integration', 'Run integration tests (mocha + db access)', - ['clean:test', 'setTestEnv', 'ensureConfig', 'mochacli:integration']); + ['test-setup', 'mochacli:integration'] + ); // ### Route tests *(sub task)* // `grunt test-routes` will run just the route tests @@ -909,7 +930,8 @@ var _ = require('lodash'), // are working as expected, including checking the headers and status codes received. It is very easy and // quick to test many permutations of routes / urls in the system. grunt.registerTask('test-routes', 'Run functional route tests (mocha)', - ['clean:test', 'setTestEnv', 'ensureConfig', 'mochacli:routes']); + ['test-setup', 'mochacli:routes'] + ); // ### Module tests *(sub task)* // `grunt test-module` will run just the module tests @@ -917,16 +939,13 @@ var _ = require('lodash'), // The purpose of the module tests is to ensure that Ghost can be used as an npm module and exposes all // required methods to interact with it. grunt.registerTask('test-module', 'Run functional module tests (mocha)', - ['clean:test', 'setTestEnv', 'ensureConfig', 'mochacli:module']); + ['test-setup', 'mochacli:module'] + ); - // ### Functional tests for the setup process - // `grunt test-functional-setup will run just the functional tests for the setup page. - // - // Setup only works with a brand new database, so it needs to run isolated from the rest of - // the functional tests. - grunt.registerTask('test-functional-setup', 'Run functional tests for setup', - ['clean:test', 'setTestEnv', 'ensureConfig', 'cleanDatabase', 'express:test', - 'spawnCasperJS:setup', 'express:test:stop'] + // ### Ember unit tests *(sub task)* + // `grunt testem` will run just the ember unit tests + grunt.registerTask('testem', 'Run the ember unit tests', + ['test-setup', 'shell:testem'] ); // ### Functional tests *(sub task)* @@ -948,8 +967,16 @@ var _ = require('lodash'), // The purpose of the functional tests is to ensure that Ghost is working as is expected from a user perspective // including buttons and other important interactions in the admin UI. grunt.registerTask('test-functional', 'Run functional interface tests (CasperJS)', - ['clean:test', 'setTestEnv', 'ensureConfig', 'cleanDatabase', 'express:test', 'spawnCasperJS', 'express:test:stop', - 'test-functional-setup'] + ['test-setup', 'cleanDatabase', 'express:test', 'spawnCasperJS', 'express:test:stop', 'test-functional-setup'] + ); + + // ### Functional tests for the setup process + // `grunt test-functional-setup will run just the functional tests for the setup page. + // + // Setup only works with a brand new database, so it needs to run isolated from the rest of + // the functional tests. + grunt.registerTask('test-functional-setup', 'Run functional tests for setup', + ['test-setup', 'cleanDatabase', 'express:test', 'spawnCasperJS:setup', 'express:test:stop'] ); // ### Coverage @@ -962,7 +989,8 @@ var _ = require('lodash'), // // Key areas for coverage are: helpers and theme elements, apps / GDK, the api and model layers. grunt.registerTask('test-coverage', 'Generate unit and integration (mocha) tests coverage report', - ['clean:test', 'setTestEnv', 'ensureConfig', 'shell:coverage']); + ['test-setup', 'shell:coverage'] + ); // ## Building assets // @@ -1043,7 +1071,8 @@ var _ = require('lodash'), repo: 'ghost', oauthKey: oauthKey, releaseDate: ninetyDaysAgo, - count: 20 + count: 20, + retry: true }) ).then(function (results) { var contributors = results[1], @@ -1142,7 +1171,8 @@ var _ = require('lodash'), ' - Copy files to release-folder/#/#{version} directory\n' + ' - Clean out unnecessary files (travis, .git*, etc)\n' + ' - Zip files in release-folder to dist-folder/#{version} directory', - ['init', 'concat:prod', 'copy:prod', 'emberBuildProd', 'uglify:release', 'clean:release', 'copy:release', 'compress:release']); + ['init', 'concat:prod', 'copy:prod', 'emberBuildProd', 'uglify:release', 'clean:release', + 'shell:shrinkwrap', 'copy:release', 'compress:release']); }; // Export the configuration diff --git a/node_modules/ghost/README.md b/node_modules/ghost/README.md index 6b5063cd..bbe5fc85 100644 --- a/node_modules/ghost/README.md +++ b/node_modules/ghost/README.md @@ -9,7 +9,7 @@ Visit the project's website at • docs on
@@ -19,7 +21,9 @@ {{/if}}

{{name}}

-

{{bio}}

+ {{#if bio}} +

{{bio}}

+ {{/if}}
{{#if location}}{{location}}{{/if}} {{#if website}}{{website}}{{/if}} @@ -34,4 +38,4 @@ {{! The tag below includes the post loop - partials/loop.hbs }} {{> "loop"}} - \ No newline at end of file + diff --git a/node_modules/ghost/content/themes/casper/default.hbs b/node_modules/ghost/content/themes/casper/default.hbs index f0b2e054..dcb6022f 100644 --- a/node_modules/ghost/content/themes/casper/default.hbs +++ b/node_modules/ghost/content/themes/casper/default.hbs @@ -21,15 +21,21 @@ {{! Ghost outputs important style and meta data with this tag }} {{ghost_head}} - + - {{! Everything else gets inserted here }} - {{{body}}} + {{navigation}} - +
+ + {{! Everything else gets inserted here }} + {{{body}}} + + + +
{{! Ghost outputs important scripts and data with this tag }} {{ghost_foot}} diff --git a/node_modules/ghost/content/themes/casper/index.hbs b/node_modules/ghost/content/themes/casper/index.hbs index 80cf642d..223f9eda 100644 --- a/node_modules/ghost/content/themes/casper/index.hbs +++ b/node_modules/ghost/content/themes/casper/index.hbs @@ -5,7 +5,9 @@
@@ -22,4 +24,4 @@ {{! The tag below includes the post loop - partials/loop.hbs }} {{> "loop"}} - \ No newline at end of file + diff --git a/node_modules/ghost/content/themes/casper/package.json b/node_modules/ghost/content/themes/casper/package.json index 509eb5e0..8076cb59 100644 --- a/node_modules/ghost/content/themes/casper/package.json +++ b/node_modules/ghost/content/themes/casper/package.json @@ -1,4 +1,4 @@ { "name": "Casper", - "version": "1.1.5" + "version": "1.1.6" } diff --git a/node_modules/ghost/content/themes/casper/page.hbs b/node_modules/ghost/content/themes/casper/page.hbs index b1c5f520..e77a0bdb 100644 --- a/node_modules/ghost/content/themes/casper/page.hbs +++ b/node_modules/ghost/content/themes/casper/page.hbs @@ -8,15 +8,15 @@
-
- {{! Everything inside the #post tags pulls data from the post }}

{{title}}

@@ -27,6 +27,5 @@
-
{{/post}} diff --git a/node_modules/ghost/content/themes/casper/partials/navigation.hbs b/node_modules/ghost/content/themes/casper/partials/navigation.hbs new file mode 100644 index 00000000..c97d4c2c --- /dev/null +++ b/node_modules/ghost/content/themes/casper/partials/navigation.hbs @@ -0,0 +1,13 @@ + + diff --git a/node_modules/ghost/content/themes/casper/post.hbs b/node_modules/ghost/content/themes/casper/post.hbs index 6d41a915..46396081 100644 --- a/node_modules/ghost/content/themes/casper/post.hbs +++ b/node_modules/ghost/content/themes/casper/post.hbs @@ -8,13 +8,14 @@
-
@@ -74,7 +75,6 @@
-
{{/post}} diff --git a/node_modules/ghost/content/themes/casper/tag.hbs b/node_modules/ghost/content/themes/casper/tag.hbs index 56ef106c..cebbab80 100644 --- a/node_modules/ghost/content/themes/casper/tag.hbs +++ b/node_modules/ghost/content/themes/casper/tag.hbs @@ -4,8 +4,10 @@ {{! If we have a tag cover, display that - else blog cover - else nothing }}
diff --git a/node_modules/ghost/core/built/scripts/ghost-dev.js b/node_modules/ghost/core/built/scripts/ghost-dev.js index b9268729..98dfa5c0 100644 --- a/node_modules/ghost/core/built/scripts/ghost-dev.js +++ b/node_modules/ghost/core/built/scripts/ghost-dev.js @@ -236,7 +236,7 @@ define("ghost/app", var App = Ember.Application.extend({ modulePrefix: 'ghost', - Resolver: Resolver['default'] + Resolver: Resolver.default }); // Runtime configuration of Ember.Application @@ -1054,6 +1054,137 @@ define("ghost/components/gh-modal-dialog", __exports__["default"] = ModalDialog; }); +define("ghost/components/gh-navitem-url-input", + ["exports"], + function(__exports__) { + "use strict"; + function joinUrlParts(url, path) { + if (path[0] !== '/' && url.slice(-1) !== '/') { + path = '/' + path; + } else if (path[0] === '/' && url.slice(-1) === '/') { + path = path.slice(1); + } + + return url + path; + } + + var NavItemUrlInputComponent = Ember.TextField.extend({ + classNameBindings: ['fakePlaceholder'], + + isBaseUrl: Ember.computed('baseUrl', 'value', function () { + return this.get('baseUrl') === this.get('value'); + }), + + fakePlaceholder: Ember.computed('isBaseUrl', 'hasFocus', function () { + return this.get('isBaseUrl') && this.get('last') && !this.get('hasFocus'); + }), + + isRelative: Ember.computed('value', function () { + return !validator.isURL(this.get('value')); + }), + + didInsertElement: function () { + var url = this.get('url'), + baseUrl = this.get('baseUrl'); + + this.set('value', url); + + // if we have a relative url, create the absolute url to be displayed in the input + if (this.get('isRelative')) { + url = joinUrlParts(baseUrl, url); + this.set('value', url); + } + }, + + focusIn: function (event) { + this.set('hasFocus', true); + + if (this.get('isBaseUrl')) { + // position the cursor at the end of the input + Ember.run.next(function (el) { + var length = el.value.length; + + el.setSelectionRange(length, length); + }, event.target); + } + }, + + keyDown: function (event) { + // delete the "placeholder" value all at once + if (this.get('isBaseUrl') && (event.keyCode === 8 || event.keyCode === 46)) { + this.set('value', ''); + + event.preventDefault(); + } + }, + + keyPress: function (event) { + // enter key + if (event.keyCode === 13) { + event.preventDefault(); + this.notifyUrlChanged(); + } + + return true; + }, + + focusOut: function () { + this.set('hasFocus', false); + + this.notifyUrlChanged(); + }, + + notifyUrlChanged: function () { + this.set('value', this.get('value').trim()); + + var url = this.get('value'), + baseUrl = this.get('baseUrl'); + + if (this.get('isRelative')) { + this.set('value', joinUrlParts(baseUrl, url)); + } + + this.sendAction('change', url); + } + }); + + __exports__["default"] = NavItemUrlInputComponent; + }); +define("ghost/components/gh-navitem", + ["exports"], + function(__exports__) { + "use strict"; + var NavItemComponent = Ember.Component.extend({ + classNames: 'navigation-item', + + attributeBindings: ['order:data-order'], + order: Ember.computed.readOnly('navItem.order'), + + keyPress: function (event) { + // enter key + if (event.keyCode === 13) { + event.preventDefault(); + this.get('controller').send('addItem'); + } + }, + + actions: { + addItem: function () { + this.sendAction('addItem'); + }, + + deleteItem: function (item) { + this.sendAction('deleteItem', item); + }, + + updateUrl: function (value) { + this.sendAction('updateUrl', value, this.get('navItem')); + } + } + }); + + __exports__["default"] = NavItemComponent; + }); define("ghost/components/gh-notification", ["exports"], function(__exports__) { @@ -1447,16 +1578,20 @@ define("ghost/components/gh-trim-focus-input", attributeBindings: ['autofocus'], autofocus: Ember.computed(function () { - return (device.ios()) ? false : 'autofocus'; + if (this.get('focus')) { + return (device.ios()) ? false : 'autofocus'; + } + + return false; }), - setFocus: function () { + didInsertElement: function () { // This fix is required until Mobile Safari has reliable // autofocus, select() or focus() support - if (this.focus && !device.ios()) { + if (this.get('focus') && !device.ios()) { this.$().val(this.$().val()).focus(); } - }.on('didInsertElement'), + }, focusOut: function () { var text = this.$().val(); @@ -1468,11 +1603,12 @@ define("ghost/components/gh-trim-focus-input", __exports__["default"] = TrimFocusInput; }); define("ghost/components/gh-upload-modal", - ["ghost/components/gh-modal-dialog","ghost/assets/lib/uploader","exports"], - function(__dependency1__, __dependency2__, __exports__) { + ["ghost/components/gh-modal-dialog","ghost/assets/lib/uploader","ghost/utils/caja-sanitizers","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { "use strict"; var ModalDialog = __dependency1__["default"]; var upload = __dependency2__["default"]; + var cajaSanitizers = __dependency3__["default"]; var UploadModal = ModalDialog.extend({ layoutName: 'components/gh-modal-dialog', @@ -1481,6 +1617,16 @@ define("ghost/components/gh-upload-modal", this._super(); upload.call(this.$('.js-drop-zone'), {fileStorage: this.get('config.fileStorage')}); }, + keyDown: function () { + this.setErrorState(false); + }, + setErrorState: function (state) { + if (state) { + this.$('.js-upload-url').addClass('error'); + } else { + this.$('.js-upload-url').removeClass('error'); + } + }, confirm: { reject: { func: function () { // The function called on rejection @@ -1491,15 +1637,23 @@ define("ghost/components/gh-upload-modal", }, accept: { buttonClass: 'btn btn-blue right', - text: 'Save', // The accept button texttext: 'Save' + text: 'Save', // The accept button text: 'Save' func: function () { - var imageType = 'model.' + this.get('imageType'); + var imageType = 'model.' + this.get('imageType'), + value; if (this.$('.js-upload-url').val()) { - this.set(imageType, this.$('.js-upload-url').val()); + value = this.$('.js-upload-url').val(); + + if (!Ember.isEmpty(value) && !cajaSanitizers.url(value)) { + this.setErrorState(true); + return {message: 'Image URI is not valid'}; + } } else { - this.set(imageType, this.$('.js-upload-target').attr('src')); + value = this.$('.js-upload-target').attr('src'); } + + this.set(imageType, value); return true; } } @@ -1510,12 +1664,17 @@ define("ghost/components/gh-upload-modal", this.sendAction(); }, confirm: function (type) { - var func = this.get('confirm.' + type + '.func'); + var result, + func = this.get('confirm.' + type + '.func'); + if (typeof func === 'function') { - func.apply(this); + result = func.apply(this); + } + + if (!result.message) { + this.sendAction(); + this.sendAction('confirm' + type); } - this.sendAction(); - this.sendAction('confirm' + type); } } }); @@ -1531,6 +1690,10 @@ define("ghost/components/gh-uploader", var PostImageUploader = Ember.Component.extend({ classNames: ['image-uploader', 'js-post-image-upload'], + imageSource: Ember.computed('image', function () { + return this.get('image') || ''; + }), + setup: function () { var $this = this.$(), self = this; @@ -1573,22 +1736,21 @@ define("ghost/components/gh-url-preview", classNames: 'ghost-url-preview', prefix: null, slug: null, - theUrl: null, - generateUrl: function () { + url: Ember.computed('slug', function () { // Get the blog URL and strip the scheme var blogUrl = this.get('config').blogUrl, noSchemeBlogUrl = blogUrl.substr(blogUrl.indexOf('://') + 3), // Remove `http[s]://` // Get the prefix and slug values prefix = this.get('prefix') ? this.get('prefix') + '/' : '', - slug = this.get('slug') ? this.get('slug') : '', + slug = this.get('slug') ? this.get('slug') + '/' : '', // Join parts of the URL together with slashes theUrl = noSchemeBlogUrl + '/' + prefix + slug; - this.set('the-url', theUrl); - }.on('didInsertElement').observes('slug') + return theUrl; + }) }); __exports__["default"] = urlPreview; @@ -1626,10 +1788,18 @@ define("ghost/controllers/application", showGlobalMobileNav: false, showSettingsMenu: false, - userImageAlt: Ember.computed('session.user.name', function () { + userImage: Ember.computed('session.user.image', function () { + return this.get('session.user.image') || this.get('ghostPaths.url').asset('/shared/img/user-image.png'); + }), + + userImageBackground: Ember.computed('userImage', function () { + return 'background-image: url(' + this.get('userImage') + ')'; + }), + + userImageAlt: Ember.computed('session.user.name', function () { var name = this.get('session.user.name'); - return name + '\'s profile picture'; + return (name) ? name + '\'s profile picture' : 'Profile picture'; }), actions: { @@ -2126,14 +2296,18 @@ define("ghost/controllers/modals/leave-editor", __exports__["default"] = LeaveEditorController; }); define("ghost/controllers/modals/signin", - ["ghost/controllers/signin","exports"], + ["ghost/mixins/validation-engine","exports"], function(__dependency1__, __exports__) { "use strict"; - var SigninController = __dependency1__["default"]; + var ValidationEngine = __dependency1__["default"]; - __exports__["default"] = SigninController.extend({ + __exports__["default"] = Ember.Controller.extend(SimpleAuth.AuthenticationControllerMixin, ValidationEngine, { needs: 'application', + authenticator: 'simple-auth-authenticator:oauth2-password-grant', + + validationType: 'signin', + identification: Ember.computed('session.user.email', function () { return this.get('session.user.email'); }), @@ -2145,15 +2319,34 @@ define("ghost/controllers/modals/signin", appController.set('skipAuthSuccessHandler', true); - this._super().then(function () { + this._super(this.getProperties('identification', 'password')).then(function () { self.send('closeModal'); self.notifications.showSuccess('Login successful.'); self.set('password', ''); + }).catch(function () { + // if authentication fails a rejected promise will be returned. + // it needs to be caught so it doesn't generate an exception in the console, + // but it's actually "handled" by the sessionAuthenticationFailed action handler. }).finally(function () { appController.set('skipAuthSuccessHandler', undefined); }); }, + validateAndAuthenticate: function () { + var self = this; + + // Manually trigger events for input fields, ensuring legacy compatibility with + // browsers and password managers that don't send proper events on autofill + $('#login').find('input').trigger('change'); + + this.validate({format: false}).then(function () { + self.notifications.closePassive(); + self.send('authenticate'); + }).catch(function (errors) { + self.notifications.showErrors(errors); + }); + }, + confirmAccept: function () { this.send('validateAndAuthenticate'); } @@ -2377,7 +2570,7 @@ define("ghost/controllers/post-settings-menu", if (metaTitle.length > 70) { metaTitle = metaTitle.substring(0, 70).trim(); metaTitle = Ember.Handlebars.Utils.escapeExpression(metaTitle); - metaTitle = new Ember.Handlebars.SafeString(metaTitle + '…'); + metaTitle = Ember.String.htmlSafe(metaTitle + '…'); } return metaTitle; @@ -2413,7 +2606,7 @@ define("ghost/controllers/post-settings-menu", // Limit to 156 characters placeholder = placeholder.substring(0, 156).trim(); placeholder = Ember.Handlebars.Utils.escapeExpression(placeholder); - placeholder = new Ember.Handlebars.SafeString(placeholder + '…'); + placeholder = Ember.String.htmlSafe(placeholder + '…'); } return placeholder; @@ -2431,7 +2624,7 @@ define("ghost/controllers/post-settings-menu", if (seoURL.length > 70) { seoURL = seoURL.substring(0, 70).trim(); - seoURL = new Ember.Handlebars.SafeString(seoURL + '…'); + seoURL = Ember.String.htmlSafe(seoURL + '…'); } return seoURL; @@ -2452,7 +2645,7 @@ define("ghost/controllers/post-settings-menu", // generate a slug if a post is new and doesn't have a title yet or // if the title is still '(Untitled)' and the slug is unaltered. if ((this.get('model.isNew') && !title) || title === '(Untitled)') { - debounceId = Ember.run.debounce(this, 'generateAndSetSlug', ['slug'], 700); + debounceId = Ember.run.debounce(this, 'generateAndSetSlug', 'model.slug', 700); } this.set('debounceId', debounceId); @@ -2945,16 +3138,14 @@ define("ghost/controllers/post-tags-input", makeSuggestionObject: function (matchingTag, _searchTerm) { var searchTerm = Ember.Handlebars.Utils.escapeExpression(_searchTerm), - // jscs:disable regexEscapedSearchTerm = searchTerm.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), - // jscs:enable tagName = Ember.Handlebars.Utils.escapeExpression(matchingTag.get('name')), regex = new RegExp('(' + regexEscapedSearchTerm + ')', 'gi'), highlightedName, suggestion = Ember.Object.create(); highlightedName = tagName.replace(regex, '$1'); - highlightedName = new Ember.Handlebars.SafeString(highlightedName); + highlightedName = Ember.String.htmlSafe(highlightedName); suggestion.set('tag', matchingTag); suggestion.set('highlightedName', highlightedName); @@ -3064,6 +3255,18 @@ define("ghost/controllers/posts/post", isPublished: Ember.computed.equal('model.status', 'published'), classNameBindings: ['model.featured'], + authorName: Ember.computed('model.author.name', 'model.author.email', function () { + return this.get('model.author.name') || this.get('model.author.email'); + }), + + authorAvatar: Ember.computed('model.author.image', function () { + return this.get('model.author.image') || this.get('ghostPaths.url').asset('/shared/img/user-image.png'); + }), + + authorAvatarBackground: Ember.computed('authorAvatar', function () { + return 'background-image: url(' + this.get('authorAvatar') + ')'; + }), + actions: { toggleFeatured: function () { var options = {disableNProgress: true}, @@ -3095,7 +3298,7 @@ define("ghost/controllers/reset", newPassword: '', ne2Password: '', token: '', - submitButtonDisabled: false, + submitting: false, validationType: 'reset', @@ -3164,6 +3367,9 @@ define("ghost/controllers/settings", showTags: Ember.computed('session.user.name', function () { return this.get('session.user.isAuthor') ? false : true; }), + showNavigation: Ember.computed('session.user.name', function () { + return this.get('session.user.isAuthor') || this.get('session.user.isEditor') ? false : true; + }), showCodeInjection: Ember.computed('session.user.name', 'controllers.feature.codeInjectionUI', function () { return this.get('session.user.isAuthor') || this.get('session.user.isEditor') || !this.get('controllers.feature.codeInjectionUI') ? false : true; }), @@ -3276,6 +3482,14 @@ define("ghost/controllers/settings/general", var SettingsGeneralController = Ember.Controller.extend({ selectedTheme: null, + logoImageSource: Ember.computed('model.logo', function () { + return this.get('model.logo') || ''; + }), + + coverImageSource: Ember.computed('model.cover', function () { + return this.get('model.cover') || ''; + }), + isDatedPermalinks: Ember.computed('model.permalinks', function (key, value) { // setter if (arguments.length > 1) { @@ -3439,6 +3653,168 @@ define("ghost/controllers/settings/labs", __exports__["default"] = LabsController; }); +define("ghost/controllers/settings/navigation", + ["exports"], + function(__exports__) { + "use strict"; + var NavigationController, + NavItem; + + NavItem = Ember.Object.extend({ + label: '', + url: '', + last: false, + + isComplete: Ember.computed('label', 'url', function () { + return !(Ember.isBlank(this.get('label').trim()) || Ember.isBlank(this.get('url'))); + }) + }); + + NavigationController = Ember.Controller.extend({ + blogUrl: Ember.computed('config.blogUrl', function () { + var url = this.get('config.blogUrl'); + + return url.slice(-1) !== '/' ? url + '/' : url; + }), + + navigationItems: Ember.computed('model.navigation', function () { + var navItems, + lastItem; + + try { + navItems = JSON.parse(this.get('model.navigation') || [{}]); + } catch (e) { + navItems = [{}]; + } + + navItems = navItems.map(function (item) { + return NavItem.create(item); + }); + + lastItem = navItems.get('lastObject'); + if (!lastItem || lastItem.get('isComplete')) { + navItems.addObject(NavItem.create({last: true})); + } + + return navItems; + }), + + updateLastNavItem: Ember.observer('navigationItems.[]', function () { + var navItems = this.get('navigationItems'); + + navItems.forEach(function (item, index, items) { + if (index === (items.length - 1)) { + item.set('last', true); + } else { + item.set('last', false); + } + }); + }), + + actions: { + addItem: function () { + var navItems = this.get('navigationItems'), + lastItem = navItems.get('lastObject'); + + if (lastItem && lastItem.get('isComplete')) { + navItems.addObject(NavItem.create({last: true})); // Adds new blank navItem + } + }, + + deleteItem: function (item) { + if (!item) { + return; + } + + var navItems = this.get('navigationItems'); + + navItems.removeObject(item); + }, + + moveItem: function (index, newIndex) { + var navItems = this.get('navigationItems'), + item = navItems.objectAt(index); + + navItems.removeAt(index); + navItems.insertAt(newIndex, item); + }, + + updateUrl: function (url, navItem) { + if (!navItem) { + return; + } + + if (Ember.isBlank(url)) { + navItem.set('url', this.get('blogUrl')); + + return; + } + + navItem.set('url', url); + }, + + save: function () { + var self = this, + navSetting, + blogUrl = this.get('config').blogUrl, + blogUrlRegex = new RegExp('^' + blogUrl + '(.*)', 'i'), + navItems = this.get('navigationItems'), + match; + + // Don't save if there's a blank label. + if (navItems.find(function (item) { return !item.get('isComplete') && !item.get('last');})) { + self.notifications.showErrors(['One of your navigation items has an empty label.
Please enter a new label or delete the item before saving.']); + return; + } + + navSetting = navItems.map(function (item) { + var label, + url; + + if (!item || !item.get('isComplete')) { + return; + } + + label = item.get('label').trim(); + url = item.get('url').trim(); + + // is this an internal URL? + match = url.match(blogUrlRegex); + + if (match) { + url = match[1]; + + // if the last char is not a slash, then add one, + // this also handles the empty case for the homepage + if (url[url.length - 1] !== '/') { + url += '/'; + } + } else if (!validator.isURL(url) && url !== '' && url[0] !== '/') { + url = '/' + url; + } + + return {label: label, url: url}; + }).compact(); + + this.set('model.navigation', JSON.stringify(navSetting)); + + // trigger change event because even if the final JSON is unchanged + // we need to have navigationItems recomputed. + this.get('model').notifyPropertyChange('navigation'); + + this.notifications.closePassive(); + + this.get('model').save().then(function () { + self.notifications.showSuccess('Navigation items saved.'); + }).catch(function (err) { + self.notifications.showErrors(err); + }); + } + } + }); + + __exports__["default"] = NavigationController; + }); define("ghost/controllers/settings/tags", ["ghost/mixins/pagination-controller","ghost/mixins/settings-menu-controller","ghost/utils/bound-one-way","exports"], function(__dependency1__, __dependency2__, __dependency3__, __exports__) { @@ -3497,7 +3873,7 @@ define("ghost/controllers/settings/tags", if (metaTitle && metaTitle.length > 70) { metaTitle = metaTitle.substring(0, 70).trim(); metaTitle = Ember.Handlebars.Utils.escapeExpression(metaTitle); - metaTitle = new Ember.Handlebars.SafeString(metaTitle + '…'); + metaTitle = Ember.String.htmlSafe(metaTitle + '…'); } return metaTitle; @@ -3515,7 +3891,7 @@ define("ghost/controllers/settings/tags", if (seoURL.length > 70) { seoURL = seoURL.substring(0, 70).trim(); - seoURL = new Ember.Handlebars.SafeString(seoURL + '…'); + seoURL = Ember.String.htmlSafe(seoURL + '…'); } return seoURL; @@ -3529,7 +3905,7 @@ define("ghost/controllers/settings/tags", if (metaDescription && metaDescription.length > 156) { metaDescription = metaDescription.substring(0, 156).trim(); metaDescription = Ember.Handlebars.Utils.escapeExpression(metaDescription); - metaDescription = new Ember.Handlebars.SafeString(metaDescription + '…'); + metaDescription = Ember.String.htmlSafe(metaDescription + '…'); } return metaDescription; @@ -3914,8 +4290,9 @@ define("ghost/controllers/signin", data = model.getProperties('identification', 'password'); this._super(data).catch(function () { - // If simple-auth's authenticate rejects we need to catch it - // to avoid an unhandled rejection exception. + // if authentication fails a rejected promise will be returned. + // it needs to be caught so it doesn't generate an exception in the console, + // but it's actually "handled" by the sessionAuthenticationFailed action handler. }); }, @@ -4057,8 +4434,8 @@ define("ghost/helpers/gh-blog-url", ["exports"], function(__exports__) { "use strict"; - var blogUrl = Ember.Handlebars.makeBoundHelper(function () { - return new Ember.Handlebars.SafeString(this.get('config.blogUrl')); + var blogUrl = Ember.HTMLBars.makeBoundHelper(function () { + return Ember.String.htmlSafe(this.get('config.blogUrl')); }); __exports__["default"] = blogUrl; @@ -4067,9 +4444,17 @@ define("ghost/helpers/gh-count-characters", ["exports"], function(__exports__) { "use strict"; - var countCharacters = Ember.Handlebars.makeBoundHelper(function (content) { + var countCharacters = Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) { var el = document.createElement('span'), - length = content ? content.length : 0; + length, + content; + + if (!arr || !arr.length) { + return; + } + + content = arr[0] || ''; + length = content.length; el.className = 'word-count'; @@ -4081,7 +4466,7 @@ define("ghost/helpers/gh-count-characters", el.innerHTML = 200 - length; - return new Ember.Handlebars.SafeString(el.outerHTML); + return Ember.String.htmlSafe(el.outerHTML); }); __exports__["default"] = countCharacters; @@ -4090,9 +4475,19 @@ define("ghost/helpers/gh-count-down-characters", ["exports"], function(__exports__) { "use strict"; - var countDownCharacters = Ember.Handlebars.makeBoundHelper(function (content, maxCharacters) { + var countDownCharacters = Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) { var el = document.createElement('span'), - length = content ? content.length : 0; + content, + maxCharacters, + length; + + if (!arr || arr.length < 2) { + return; + } + + content = arr[0] || ''; + maxCharacters = arr[1]; + length = content.length; el.className = 'word-count'; @@ -4104,7 +4499,7 @@ define("ghost/helpers/gh-count-down-characters", el.innerHTML = length; - return new Ember.Handlebars.SafeString(el.outerHTML); + return Ember.String.htmlSafe(el.outerHTML); }); __exports__["default"] = countDownCharacters; @@ -4115,12 +4510,21 @@ define("ghost/helpers/gh-count-words", "use strict"; var counter = __dependency1__["default"]; - var countWords = Ember.Handlebars.makeBoundHelper(function (markdown) { + var countWords = Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) { + if (!arr || !arr.length) { + return; + } + + var markdown, + count; + + markdown = arr[0] || ''; + if (/^\s*$/.test(markdown)) { return '0 words'; } - var count = counter(markdown || ''); + count = counter(markdown); return count + (count === 1 ? ' word' : ' words'); }); @@ -4131,26 +4535,28 @@ define("ghost/helpers/gh-format-html", ["ghost/utils/caja-sanitizers","exports"], function(__dependency1__, __exports__) { "use strict"; - /* global Handlebars, html_sanitize*/ + /* global html_sanitize*/ var cajaSanitizers = __dependency1__["default"]; - var formatHTML = Ember.Handlebars.makeBoundHelper(function (html) { - var escapedhtml = html || ''; + var formatHTML = Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) { + if (!arr || !arr.length) { + return; + } + + var escapedhtml = arr[0] || ''; // replace script and iFrame - // jscs:disable escapedhtml = escapedhtml.replace(/)<[^<]*)*<\/script>/gi, '
Embedded JavaScript
'); escapedhtml = escapedhtml.replace(/)<[^<]*)*<\/iframe>/gi, '
Embedded iFrame
'); - // jscs:enable // sanitize HTML // jscs:disable requireCamelCaseOrUpperCaseIdentifiers escapedhtml = html_sanitize(escapedhtml, cajaSanitizers.url, cajaSanitizers.id); // jscs:enable requireCamelCaseOrUpperCaseIdentifiers - return new Handlebars.SafeString(escapedhtml); + return Ember.String.htmlSafe(escapedhtml); }); __exports__["default"] = formatHTML; @@ -4159,7 +4565,7 @@ define("ghost/helpers/gh-format-markdown", ["ghost/utils/caja-sanitizers","exports"], function(__dependency1__, __exports__) { "use strict"; - /* global Showdown, Handlebars, html_sanitize*/ + /* global Showdown, html_sanitize*/ var cajaSanitizers = __dependency1__["default"]; var showdown, @@ -4167,26 +4573,29 @@ define("ghost/helpers/gh-format-markdown", showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']}); - formatMarkdown = Ember.Handlebars.makeBoundHelper(function (markdown) { - var escapedhtml = ''; + formatMarkdown = Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) { + if (!arr || !arr.length) { + return; + } + + var escapedhtml = '', + markdown = arr[0] || ''; // convert markdown to HTML - escapedhtml = showdown.makeHtml(markdown || ''); + escapedhtml = showdown.makeHtml(markdown); // replace script and iFrame - // jscs:disable escapedhtml = escapedhtml.replace(/)<[^<]*)*<\/script>/gi, '
Embedded JavaScript
'); escapedhtml = escapedhtml.replace(/)<[^<]*)*<\/iframe>/gi, '
Embedded iFrame
'); - // jscs:enable // sanitize html // jscs:disable requireCamelCaseOrUpperCaseIdentifiers escapedhtml = html_sanitize(escapedhtml, cajaSanitizers.url, cajaSanitizers.id); // jscs:enable requireCamelCaseOrUpperCaseIdentifiers - return new Handlebars.SafeString(escapedhtml); + return Ember.String.htmlSafe(escapedhtml); }); __exports__["default"] = formatMarkdown; @@ -4195,8 +4604,13 @@ define("ghost/helpers/gh-format-timeago", ["exports"], function(__exports__) { "use strict"; - /* global moment */ - var formatTimeago = Ember.Handlebars.makeBoundHelper(function (timeago) { + var formatTimeago = Ember.HTMLBars.makeBoundHelper(function (arr /* hashParams */) { + if (!arr || !arr.length) { + return; + } + + var timeago = arr[0]; + return moment(timeago).fromNow(); // stefanpenner says cool for small number of timeagos. // For large numbers moment sucks => single Ember.Object based clock better @@ -4258,7 +4672,7 @@ define("ghost/helpers/gh-path", base = base + url; } - return new Ember.Handlebars.SafeString(base); + return Ember.String.htmlSafe(base); } __exports__["default"] = ghostPathsHelper; @@ -4756,7 +5170,7 @@ define("ghost/mixins/editor-base-controller", if (status === 'published') { message += ' View ' + this.get('postOrPage') + ''; } - this.notifications.showSuccess(message, {delayed: delay}); + this.notifications.showSuccess(message.htmlSafe(), {delayed: delay}); }, showErrorNotification: function (prevStatus, status, errors, delay) { @@ -4765,7 +5179,7 @@ define("ghost/mixins/editor-base-controller", message += '
' + error; - this.notifications.showError(message, {delayed: delay}); + this.notifications.showError(message.htmlSafe(), {delayed: delay}); }, shouldFocusTitle: Ember.computed.alias('model.isNew'), @@ -4879,9 +5293,7 @@ define("ghost/mixins/editor-base-controller", var editor = this.get('codemirror'), line = this.findLine(Ember.$(e.currentTarget).attr('id')), lineNumber = editor.getLineNumber(line), - // jscs:disable match = line.text.match(/\([^\n]*\)?/), - // jscs:enable replacement = '(http://)'; if (match) { @@ -4891,9 +5303,7 @@ define("ghost/mixins/editor-base-controller", {line: lineNumber, ch: match.index + match[0].length - 1} ); } else { - // jscs:disable match = line.text.match(/\]/); - // jscs:enable if (match) { editor.replaceRange( replacement, @@ -5175,10 +5585,8 @@ define("ghost/mixins/marker-manager", function(__exports__) { "use strict"; var MarkerManager = Ember.Mixin.create({ - // jscs:disable imageMarkdownRegex: /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim, markerRegex: /\{<([\w\W]*?)>\}/, - // jscs:enable uploadId: 1, @@ -5340,9 +5748,7 @@ define("ghost/mixins/marker-manager", var editor = this.get('codemirror'), ln = editor.getLineNumber(line), - // jscs:disable markerRegex = /\{<([\w\W]*?)>\}/, - // jscs:enable markerText = line.text.match(markerRegex); @@ -5920,12 +6326,15 @@ define("ghost/mixins/validation-engine", // get the validator's error messages from the array. // normalize array members to map to strings. message = errors.map(function (error) { + var errorMessage; if (typeof error === 'string') { - return error; + errorMessage = error; + } else { + errorMessage = error.message; } - return error.message; - }).join('
'); + return Ember.Handlebars.Utils.escapeExpression(errorMessage); + }).join('
').htmlSafe(); } else if (errors instanceof Error) { message += errors.message || '.'; } else if (typeof errors === 'object') { @@ -6182,7 +6591,8 @@ define("ghost/models/setting", availableThemes: DS.attr(), ghost_head: DS.attr('string'), ghost_foot: DS.attr('string'), - labs: DS.attr('string') + labs: DS.attr('string'), + navigation: DS.attr('string') }); __exports__["default"] = Setting; @@ -6405,6 +6815,7 @@ define("ghost/router", this.route('tags'); this.route('labs'); this.route('code-injection'); + this.route('navigation'); }); // Redirect debug to settings labs @@ -7301,6 +7712,43 @@ define("ghost/routes/settings/labs", __exports__["default"] = LabsRoute; }); +define("ghost/routes/settings/navigation", + ["ghost/routes/authenticated","ghost/mixins/current-user-settings","ghost/mixins/style-body","exports"], + function(__dependency1__, __dependency2__, __dependency3__, __exports__) { + "use strict"; + var AuthenticatedRoute = __dependency1__["default"]; + var CurrentUserSettings = __dependency2__["default"]; + var styleBody = __dependency3__["default"]; + + var NavigationRoute = AuthenticatedRoute.extend(styleBody, CurrentUserSettings, { + + titleToken: 'Navigation', + + classNames: ['settings-view-navigation'], + + beforeModel: function () { + return this.currentUser().then(this.transitionAuthor()); + }, + + model: function () { + return this.store.find('setting', {type: 'blog,theme'}).then(function (records) { + return records.get('firstObject'); + }); + }, + + actions: { + save: function () { + // since shortcuts are run on the route, we have to signal to the components + // on the page that we're about to save. + $('.page-actions .btn-blue').focus(); + + this.get('controller').send('save'); + } + } + }); + + __exports__["default"] = NavigationRoute; + }); define("ghost/routes/settings/tags", ["ghost/routes/authenticated","ghost/mixins/current-user-settings","ghost/mixins/pagination-route","exports"], function(__dependency1__, __dependency2__, __dependency3__, __exports__) { @@ -7726,21 +8174,6 @@ define("ghost/serializers/post", return this._super.apply(this, arguments); }, - keyForAttribute: function (attr) { - return attr; - }, - - keyForRelationship: function (relationshipName) { - // this is a hack to prevent Ember-Data from deleting our `tags` reference. - // ref: https://github.com/emberjs/data/issues/2051 - // @TODO: remove this once the situation becomes clearer what to do. - if (relationshipName === 'tags') { - return 'tag'; - } - - return relationshipName; - }, - serializeIntoHash: function (hash, type, record, options) { options = options || {}; options.includeId = true; @@ -7850,21 +8283,6 @@ define("ghost/serializers/user", delete payload[pluralizedRoot]; return this._super.apply(this, arguments); - }, - - keyForAttribute: function (attr) { - return attr; - }, - - keyForRelationship: function (relationshipName) { - // this is a hack to prevent Ember-Data from deleting our `tags` reference. - // ref: https://github.com/emberjs/data/issues/2051 - // @TODO: remove this once the situation becomes clearer what to do. - if (relationshipName === 'roles') { - return 'role'; - } - - return relationshipName; } }); @@ -7897,7 +8315,7 @@ define("ghost/utils/ajax", "use strict"; /* global ic */ - var ajax = window.ajax = function () { + var ajax = function () { return ic.ajax.request.apply(null, arguments); }; @@ -8010,12 +8428,10 @@ define("ghost/utils/caja-sanitizers", * URLs are allowed if they start with http://, https://, or /. */ url = function (url) { - // jscs:disable url = url.toString().replace(/['"]+/g, ''); if (/^https?:\/\//.test(url) || /^\//.test(url)) { return url; } - // jscs:enable }; /** @@ -8135,9 +8551,7 @@ define("ghost/utils/codemirror-shortcuts", hashPrefix = new Array(currentHeaderLevel + 2).join('#'); - // jscs:disable replacementLine = hashPrefix + ' ' + line.replace(/^#* /, ''); - // jscs:enable this.replaceRange(replacementLine, fromLineStart, toLineEnd); this.setCursor(cursor.line, cursor.ch + replacementLine.length); @@ -8172,9 +8586,7 @@ define("ghost/utils/codemirror-shortcuts", return; case 'list': - // jscs:disable md = text.replace(/^(\s*)(\w\W*)/gm, '$1* $2'); - // jscs:enable this.replaceSelection(md, 'end'); return; @@ -8598,8 +9010,19 @@ define("ghost/utils/notifications", this._super(object); }, handleNotification: function (message, delayed) { - if (!message.status) { - message.status = 'passive'; + if (typeof message.toJSON === 'function') { + // If this is a persistent message from the server, treat it as html safe + if (message.get('status') === 'persistent') { + message.set('message', message.get('message').htmlSafe()); + } + + if (!message.get('status')) { + message.set('status', 'passive'); + } + } else { + if (!message.status) { + message.status = 'passive'; + } } if (!delayed) { @@ -8799,12 +9222,14 @@ define("ghost/utils/word-count", "use strict"; // jscs: disable function wordCount(s) { - s = s.replace(/(^\s*)|(\s*$)/gi, ''); // exclude start and end white-space - s = s.replace(/[ ]{2,}/gi, ' '); // 2 or more space to 1 - s = s.replace(/\n /gi, '\n'); // exclude newline with a start spacing - s = s.replace(/\n+/gi, '\n'); - - return s.split(/ |\n/).length; + s = s.replace(/<(.|\n)*?>/g, ' '); // strip tags + s = s.replace(/[^\w\s]/g, ''); // ignore non-alphanumeric letters + s = s.replace(/(^\s*)|(\s*$)/gi, ''); // exclude starting and ending white-space + s = s.replace(/\n /gi, ' '); // convert newlines to spaces + s = s.replace(/\n+/gi, ' '); + s = s.replace(/[ ]{2,}/gi, ' '); // convert 2 or more spaces to 1 + + return s.split(' ').length; } __exports__["default"] = wordCount; @@ -9122,43 +9547,59 @@ define("ghost/validators/user", __exports__["default"] = UserValidator; }); define("ghost/views/application", - ["ghost/utils/mobile","ghost/utils/bind","exports"], - function(__dependency1__, __dependency2__, __exports__) { + ["ghost/utils/mobile","exports"], + function(__dependency1__, __exports__) { "use strict"; var mobileQuery = __dependency1__["default"]; - var bind = __dependency2__["default"]; var ApplicationView = Ember.View.extend({ elementId: 'container', - setupGlobalMobileNav: function () { + didInsertElement: function () { // #### Navigating within the sidebar closes it. var self = this; + $('body').on('click tap', '.js-nav-item', function () { - if (mobileQuery.matches) { - self.set('controller.showGlobalMobileNav', false); - } + Ember.run(function () { + if (mobileQuery.matches) { + self.set('controller.showGlobalMobileNav', false); + } + }); }); // #### Close the nav if mobile and clicking outside of the nav or not the burger toggle $('.js-nav-cover').on('click tap', function () { - var isOpen = self.get('controller.showGlobalMobileNav'); - if (isOpen) { - self.set('controller.showGlobalMobileNav', false); - } + Ember.run(function () { + var isOpen = self.get('controller.showGlobalMobileNav'); + + if (isOpen) { + self.set('controller.showGlobalMobileNav', false); + } + }); }); + function swapUserMenuDropdownTriangleClasses(mq) { + if (mq.matches) { + $('.js-user-menu-dropdown-menu').removeClass('dropdown-triangle-top-right ').addClass('dropdown-triangle-bottom'); + } else { + $('.js-user-menu-dropdown-menu').removeClass('dropdown-triangle-bottom').addClass('dropdown-triangle-top-right'); + } + } + // #### Listen to the viewport and change user-menu dropdown triangle classes accordingly - mobileQuery.addListener(this.swapUserMenuDropdownTriangleClasses); - this.swapUserMenuDropdownTriangleClasses(mobileQuery); - }.on('didInsertElement'), + this.set('swapUserMenuDropdownTriangleClasses', Ember.run.bind(this, swapUserMenuDropdownTriangleClasses)); - swapUserMenuDropdownTriangleClasses: function (mq) { - if (mq.matches) { - $('.js-user-menu-dropdown-menu').removeClass('dropdown-triangle-top-right ').addClass('dropdown-triangle-bottom'); - } else { - $('.js-user-menu-dropdown-menu').removeClass('dropdown-triangle-bottom').addClass('dropdown-triangle-top-right'); - } + mobileQuery.addListener(this.get('swapUserMenuDropdownTriangleClasses')); + swapUserMenuDropdownTriangleClasses(mobileQuery); + + this.set('closeGlobalMobileNavOnDesktop', Ember.run.bind(this, function closeGlobalMobileNavOnDesktop(mq) { + if (!mq.matches) { + // Is desktop sized + this.set('controller.showGlobalMobileNav', false); + } + })); + + mobileQuery.addListener(this.get('closeGlobalMobileNavOnDesktop')); }, showGlobalMobileNavObserver: function () { @@ -9169,20 +9610,10 @@ define("ghost/views/application", } }.observes('controller.showGlobalMobileNav'), - setupCloseNavOnDesktop: function () { - this.set('closeGlobalMobileNavOnDesktop', bind(function closeGlobalMobileNavOnDesktop(mq) { - if (!mq.matches) { - // Is desktop sized - this.set('controller.showGlobalMobileNav', false); - } - }, this)); - - mobileQuery.addListener(this.closeGlobalMobileNavOnDesktop); - }.on('didInsertElement'), - - removeCloseNavOnDesktop: function () { - mobileQuery.removeListener(this.closeGlobalMobileNavOnDesktop); - }.on('willDestroyElement'), + willDestroyElement: function () { + mobileQuery.removeListener(this.get('closeGlobalMobileNavOnDesktop')); + mobileQuery.removeListener(this.get('swapUserMenuDropdownTriangleClasses')); + }, toggleSettingsMenuBodyClass: function () { $('body').toggleClass('settings-menu-expanded', this.get('controller.showSettingsMenu')); @@ -9732,6 +10163,46 @@ define("ghost/views/settings/labs", __exports__["default"] = SettingsLabsView; }); +define("ghost/views/settings/navigation", + ["ghost/views/settings/content-base","exports"], + function(__dependency1__, __exports__) { + "use strict"; + var BaseView = __dependency1__["default"]; + + var SettingsNavigationView = BaseView.extend({ + + didInsertElement: function () { + var navContainer = Ember.$('.js-settings-navigation'), + navElements = '.navigation-item:not(.navigation-item:last-child)', + self = this; + + navContainer.sortable({ + handle: '.navigation-item-drag-handle', + items: navElements, + + start: function (event, ui) { + Ember.run(function () { + ui.item.data('start-index', ui.item.index()); + }); + }, + + update: function (event, ui) { + Ember.run(function () { + self.get('controller').send('moveItem', ui.item.data('start-index'), ui.item.index()); + ui.item.remove(); + }); + } + }); + }, + + willDestroyElement: function () { + Ember.$('.js-settings-navigation').sortable('destroy'); + } + + }); + + __exports__["default"] = SettingsNavigationView; + }); define("ghost/views/settings/tags", ["ghost/views/settings/content-base","ghost/mixins/pagination-view-infinite-scroll","exports"], function(__dependency1__, __dependency2__, __exports__) { @@ -9831,7 +10302,7 @@ define("ghost/views/settings/users/users-list-view", /*global require */ if (!window.disableBoot) { - window.App = require('ghost/app')['default'].create(); + window.App = require('ghost/app').default.create(); } //# sourceMappingURL=ghost-dev.js.map \ No newline at end of file diff --git a/node_modules/ghost/core/built/scripts/ghost-tests.js b/node_modules/ghost/core/built/scripts/ghost-tests.js index 2689f282..a4b1540c 100644 --- a/node_modules/ghost/core/built/scripts/ghost-tests.js +++ b/node_modules/ghost/core/built/scripts/ghost-tests.js @@ -40,8 +40,1198 @@ define("ghost/tests/unit/components/gh-trim-focus-input_test", component.$().focusout(); expect(component.$().val()).to.equal('some random stuff'); }); + + it('does not have the autofocus attribute if not set to focus', function () { + var component = this.subject({ + value: 'some text', + focus: false + }); + + this.render(); + + expect(component.$().attr('autofocus')).to.not.be.ok; + }); + + it('has the autofocus attribute if set to focus', function () { + var component = this.subject({ + value: 'some text', + focus: true + }); + + this.render(); + + expect(component.$().attr('autofocus')).to.be.ok; + }); }); }); +define("ghost/tests/unit/components/gh-url-preview_test", + ["ember-mocha"], + function(__dependency1__) { + "use strict"; + /* jshint expr:true */ + var describeComponent = __dependency1__.describeComponent; + var it = __dependency1__.it; + + describeComponent('gh-url-preview', + function () { + it('generates the correct preview URL with a prefix', function () { + var component = this.subject({ + prefix: 'tag', + slug: 'test-slug', + tagName: 'p', + classNames: 'test-class', + + config: {blogUrl: 'http://my-ghost-blog.com'} + }); + + this.render(); + + expect(component.get('url')).to.equal('my-ghost-blog.com/tag/test-slug/'); + }); + + it('generates the correct preview URL without a prefix', function () { + var component = this.subject({ + slug: 'test-slug', + tagName: 'p', + classNames: 'test-class', + + config: {blogUrl: 'http://my-ghost-blog.com'} + }); + + this.render(); + + expect(component.get('url')).to.equal('my-ghost-blog.com/test-slug/'); + }); + } + ); + }); +define("ghost/tests/unit/controllers/post-settings-menu_test", + ["ember-mocha"], + function(__dependency1__) { + "use strict"; + /* jshint expr:true */ + var describeModule = __dependency1__.describeModule; + var it = __dependency1__.it; + + describeModule( + 'controller:post-settings-menu', + { + needs: ['controller:application'] + }, + + function () { + it('slugValue is one-way bound to model.slug', function () { + var controller = this.subject({ + model: Ember.Object.create({ + slug: 'a-slug' + }) + }); + + expect(controller.get('model.slug')).to.equal('a-slug'); + expect(controller.get('slugValue')).to.equal('a-slug'); + + Ember.run(function () { + controller.set('model.slug', 'changed-slug'); + + expect(controller.get('slugValue')).to.equal('changed-slug'); + }); + + Ember.run(function () { + controller.set('slugValue', 'changed-directly'); + + expect(controller.get('model.slug')).to.equal('changed-slug'); + expect(controller.get('slugValue')).to.equal('changed-directly'); + }); + + Ember.run(function () { + // test that the one-way binding is still in place + controller.set('model.slug', 'should-update'); + + expect(controller.get('slugValue')).to.equal('should-update'); + }); + }); + + it('metaTitleScratch is one-way bound to model.meta_title', function () { + var controller = this.subject({ + model: Ember.Object.create({ + meta_title: 'a title' + }) + }); + + expect(controller.get('model.meta_title')).to.equal('a title'); + expect(controller.get('metaTitleScratch')).to.equal('a title'); + + Ember.run(function () { + controller.set('model.meta_title', 'a different title'); + + expect(controller.get('metaTitleScratch')).to.equal('a different title'); + }); + + Ember.run(function () { + controller.set('metaTitleScratch', 'changed directly'); + + expect(controller.get('model.meta_title')).to.equal('a different title'); + expect(controller.get('metaTitleScratch')).to.equal('changed directly'); + }); + + Ember.run(function () { + // test that the one-way binding is still in place + controller.set('model.meta_title', 'should update'); + + expect(controller.get('metaTitleScratch')).to.equal('should update'); + }); + }); + + it('metaDescriptionScratch is one-way bound to model.meta_description', function () { + var controller = this.subject({ + model: Ember.Object.create({ + meta_description: 'a description' + }) + }); + + expect(controller.get('model.meta_description')).to.equal('a description'); + expect(controller.get('metaDescriptionScratch')).to.equal('a description'); + + Ember.run(function () { + controller.set('model.meta_description', 'a different description'); + + expect(controller.get('metaDescriptionScratch')).to.equal('a different description'); + }); + + Ember.run(function () { + controller.set('metaDescriptionScratch', 'changed directly'); + + expect(controller.get('model.meta_description')).to.equal('a different description'); + expect(controller.get('metaDescriptionScratch')).to.equal('changed directly'); + }); + + Ember.run(function () { + // test that the one-way binding is still in place + controller.set('model.meta_description', 'should update'); + + expect(controller.get('metaDescriptionScratch')).to.equal('should update'); + }); + }); + + describe('seoTitle', function () { + it('should be the meta_title if one exists', function () { + var controller = this.subject({ + model: Ember.Object.create({ + meta_title: 'a meta-title', + titleScratch: 'should not be used' + }) + }); + + expect(controller.get('seoTitle')).to.equal('a meta-title'); + }); + + it('should default to the title if an explicit meta-title does not exist', function () { + var controller = this.subject({ + model: Ember.Object.create({ + titleScratch: 'should be the meta-title' + }) + }); + + expect(controller.get('seoTitle')).to.equal('should be the meta-title'); + }); + + it('should be the meta_title if both title and meta_title exist', function () { + var controller = this.subject({ + model: Ember.Object.create({ + meta_title: 'a meta-title', + titleScratch: 'a title' + }) + }); + + expect(controller.get('seoTitle')).to.equal('a meta-title'); + }); + + it('should revert to the title if explicit meta_title is removed', function () { + var controller = this.subject({ + model: Ember.Object.create({ + meta_title: 'a meta-title', + titleScratch: 'a title' + }) + }); + + expect(controller.get('seoTitle')).to.equal('a meta-title'); + + Ember.run(function () { + controller.set('model.meta_title', ''); + + expect(controller.get('seoTitle')).to.equal('a title'); + }); + }); + + it('should truncate to 70 characters with an appended ellipsis', function () { + var longTitle, + controller; + + longTitle = new Array(100).join('a'); + expect(longTitle.length).to.equal(99); + + controller = this.subject({ + model: Ember.Object.create() + }); + + Ember.run(function () { + var expected = longTitle.substr(0, 70) + '…'; + + controller.set('metaTitleScratch', longTitle); + + expect(controller.get('seoTitle').toString().length).to.equal(78); + expect(controller.get('seoTitle').toString()).to.equal(expected); + }); + }); + }); + + describe('seoDescription', function () { + it('should be the meta_description if one exists', function () { + var controller = this.subject({ + model: Ember.Object.create({ + meta_description: 'a description' + }) + }); + + expect(controller.get('seoDescription')).to.equal('a description'); + }); + + it.skip('should be generated from the rendered markdown if not explicitly set', function () { + // can't test right now because the rendered markdown is being pulled + // from the DOM via jquery + }); + + it('should truncate to 156 characters with an appended ellipsis', function () { + var longDescription, + controller; + + longDescription = new Array(200).join('a'); + expect(longDescription.length).to.equal(199); + + controller = this.subject({ + model: Ember.Object.create() + }); + + Ember.run(function () { + var expected = longDescription.substr(0, 156) + '…'; + + controller.set('metaDescriptionScratch', longDescription); + + expect(controller.get('seoDescription').toString().length).to.equal(164); + expect(controller.get('seoDescription').toString()).to.equal(expected); + }); + }); + }); + + describe('seoURL', function () { + it('should be the URL of the blog if no post slug exists', function () { + var controller = this.subject({ + config: Ember.Object.create({blogUrl: 'http://my-ghost-blog.com'}), + model: Ember.Object.create() + }); + + expect(controller.get('seoURL')).to.equal('http://my-ghost-blog.com/'); + }); + + it('should be the URL of the blog plus the post slug', function () { + var controller = this.subject({ + config: Ember.Object.create({blogUrl: 'http://my-ghost-blog.com'}), + model: Ember.Object.create({slug: 'post-slug'}) + }); + + expect(controller.get('seoURL')).to.equal('http://my-ghost-blog.com/post-slug/'); + }); + + it('should update when the post slug changes', function () { + var controller = this.subject({ + config: Ember.Object.create({blogUrl: 'http://my-ghost-blog.com'}), + model: Ember.Object.create({slug: 'post-slug'}) + }); + + expect(controller.get('seoURL')).to.equal('http://my-ghost-blog.com/post-slug/'); + + Ember.run(function () { + controller.set('model.slug', 'changed-slug'); + + expect(controller.get('seoURL')).to.equal('http://my-ghost-blog.com/changed-slug/'); + }); + }); + + it('should truncate a long URL to 70 characters with an appended ellipsis', function () { + var longSlug, + blogURL = 'http://my-ghost-blog.com', + expected, + controller; + + longSlug = new Array(75).join('a'); + expect(longSlug.length).to.equal(74); + + controller = this.subject({ + config: Ember.Object.create({blogUrl: blogURL}), + model: Ember.Object.create({slug: longSlug}) + }); + + expected = blogURL + '/' + longSlug + '/'; + expected = expected.substr(0, 70) + '…'; + + expect(controller.get('seoURL').toString().length).to.equal(78); + expect(controller.get('seoURL').toString()).to.equal(expected); + }); + }); + + describe('togglePage', function () { + it('should toggle the page property', function () { + var controller = this.subject({ + model: Ember.Object.create({ + page: false, + isNew: true + }) + }); + + expect(controller.get('model.page')).to.not.be.ok; + + Ember.run(function () { + controller.send('togglePage'); + + expect(controller.get('model.page')).to.be.ok; + }); + }); + + it('should not save the post if it is still new', function () { + var controller = this.subject({ + model: Ember.Object.create({ + page: false, + isNew: true, + save: function () { + this.incrementProperty('saved'); + return Ember.RSVP.resolve(); + } + }) + }); + + Ember.run(function () { + controller.send('togglePage'); + + expect(controller.get('model.page')).to.be.ok; + expect(controller.get('model.saved')).to.not.be.ok; + }); + }); + + it('should save the post if it is not new', function () { + var controller = this.subject({ + model: Ember.Object.create({ + page: false, + isNew: false, + save: function () { + this.incrementProperty('saved'); + return Ember.RSVP.resolve(); + } + }) + }); + + Ember.run(function () { + controller.send('togglePage'); + + expect(controller.get('model.page')).to.be.ok; + expect(controller.get('model.saved')).to.equal(1); + }); + }); + }); + + describe('toggleFeatured', function () { + it('should toggle the featured property', function () { + var controller = this.subject({ + model: Ember.Object.create({ + featured: false, + isNew: true + }) + }); + + Ember.run(function () { + controller.send('toggleFeatured'); + + expect(controller.get('model.featured')).to.be.ok; + }); + }); + + it('should not save the post if it is still new', function () { + var controller = this.subject({ + model: Ember.Object.create({ + featured: false, + isNew: true, + save: function () { + this.incrementProperty('saved'); + return Ember.RSVP.resolve(); + } + }) + }); + + Ember.run(function () { + controller.send('toggleFeatured'); + + expect(controller.get('model.featured')).to.be.ok; + expect(controller.get('model.saved')).to.not.be.ok; + }); + }); + + it('should save the post if it is not new', function () { + var controller = this.subject({ + model: Ember.Object.create({ + featured: false, + isNew: false, + save: function () { + this.incrementProperty('saved'); + return Ember.RSVP.resolve(); + } + }) + }); + + Ember.run(function () { + controller.send('toggleFeatured'); + + expect(controller.get('model.featured')).to.be.ok; + expect(controller.get('model.saved')).to.equal(1); + }); + }); + }); + + describe('generateAndSetSlug', function () { + it('should generate a slug and set it on the destination', function (done) { + var controller = this.subject({ + slugGenerator: Ember.Object.create({ + generateSlug: function (str) { + return Ember.RSVP.resolve(str + '-slug'); + } + }), + model: Ember.Object.create({slug: ''}) + }); + + Ember.run(function () { + controller.set('model.titleScratch', 'title'); + controller.generateAndSetSlug('model.slug'); + + expect(controller.get('model.slug')).to.equal(''); + + Ember.RSVP.resolve(controller.get('lastPromise')).then(function () { + expect(controller.get('model.slug')).to.equal('title-slug'); + + done(); + }).catch(done); + }); + }); + + it('should not set the destination if the title is "(Untitled)" and the post already has a slug', function (done) { + var controller = this.subject({ + slugGenerator: Ember.Object.create({ + generateSlug: function (str) { + return Ember.RSVP.resolve(str + '-slug'); + } + }), + model: Ember.Object.create({ + slug: 'whatever' + }) + }); + + expect(controller.get('model.slug')).to.equal('whatever'); + + Ember.run(function () { + controller.set('model.titleScratch', 'title'); + + Ember.RSVP.resolve(controller.get('lastPromise')).then(function () { + expect(controller.get('model.slug')).to.equal('whatever'); + + done(); + }).catch(done); + }); + }); + }); + + describe('titleObserver', function () { + it('should invoke generateAndSetSlug if the post is new and a title has not been set', function (done) { + var controller = this.subject({ + model: Ember.Object.create({isNew: true}), + invoked: 0, + generateAndSetSlug: function () { + this.incrementProperty('invoked'); + } + }); + + expect(controller.get('invoked')).to.equal(0); + expect(controller.get('model.title')).to.not.be.ok; + + Ember.run(function () { + controller.set('model.titleScratch', 'test'); + + controller.titleObserver(); + + // since titleObserver invokes generateAndSetSlug with a delay of 700ms + // we need to make sure this assertion runs after that. + // probably a better way to handle this? + Ember.run.later(function () { + expect(controller.get('invoked')).to.equal(1); + + done(); + }, 800); + }); + }); + + it('should invoke generateAndSetSlug if the post title is "(Untitled)"', function (done) { + var controller = this.subject({ + model: Ember.Object.create({ + isNew: false, + title: '(Untitled)' + }), + invoked: 0, + generateAndSetSlug: function () { + this.incrementProperty('invoked'); + } + }); + + expect(controller.get('invoked')).to.equal(0); + expect(controller.get('model.title')).to.equal('(Untitled)'); + + Ember.run(function () { + controller.set('model.titleScratch', 'test'); + + controller.titleObserver(); + + // since titleObserver invokes generateAndSetSlug with a delay of 700ms + // we need to make sure this assertion runs after that. + // probably a better way to handle this? + Ember.run.later(function () { + expect(controller.get('invoked')).to.equal(1); + + done(); + }, 800); + }); + }); + + it('should not invoke generateAndSetSlug if the post is new but has a title', function (done) { + var controller = this.subject({ + model: Ember.Object.create({ + isNew: true, + title: 'a title' + }), + invoked: 0, + generateAndSetSlug: function () { + this.incrementProperty('invoked'); + } + }); + + expect(controller.get('invoked')).to.equal(0); + expect(controller.get('model.title')).to.equal('a title'); + + Ember.run(function () { + controller.set('model.titleScratch', 'test'); + + controller.titleObserver(); + + // since titleObserver invokes generateAndSetSlug with a delay of 700ms + // we need to make sure this assertion runs after that. + // probably a better way to handle this? + Ember.run.later(function () { + expect(controller.get('invoked')).to.equal(0); + + done(); + }, 800); + }); + }); + }); + + describe('updateSlug', function () { + it('should reset slugValue to the previous slug when the new slug is blank or unchanged', function () { + var controller = this.subject({ + model: Ember.Object.create({ + slug: 'slug' + }) + }); + + Ember.run(function () { + // unchanged + controller.set('slugValue', 'slug'); + controller.send('updateSlug', controller.get('slugValue')); + + expect(controller.get('model.slug')).to.equal('slug'); + expect(controller.get('slugValue')).to.equal('slug'); + }); + + Ember.run(function () { + // unchanged after trim + controller.set('slugValue', 'slug '); + controller.send('updateSlug', controller.get('slugValue')); + + expect(controller.get('model.slug')).to.equal('slug'); + expect(controller.get('slugValue')).to.equal('slug'); + }); + + Ember.run(function () { + // blank + controller.set('slugValue', ''); + controller.send('updateSlug', controller.get('slugValue')); + + expect(controller.get('model.slug')).to.equal('slug'); + expect(controller.get('slugValue')).to.equal('slug'); + }); + }); + + it('should not set a new slug if the server-generated slug matches existing slug', function (done) { + var controller = this.subject({ + slugGenerator: Ember.Object.create({ + generateSlug: function (str) { + var promise; + promise = Ember.RSVP.resolve(str.split('#')[0]); + this.set('lastPromise', promise); + return promise; + } + }), + model: Ember.Object.create({ + slug: 'whatever' + }) + }); + + Ember.run(function () { + controller.set('slugValue', 'whatever#slug'); + controller.send('updateSlug', controller.get('slugValue')); + + Ember.RSVP.resolve(controller.get('lastPromise')).then(function () { + expect(controller.get('model.slug')).to.equal('whatever'); + + done(); + }).catch(done); + }); + }); + + it('should not set a new slug if the only change is to the appended increment value', function (done) { + var controller = this.subject({ + slugGenerator: Ember.Object.create({ + generateSlug: function (str) { + var promise; + promise = Ember.RSVP.resolve(str.replace(/[^a-zA-Z]/g, '') + '-2'); + this.set('lastPromise', promise); + return promise; + } + }), + model: Ember.Object.create({ + slug: 'whatever' + }) + }); + + Ember.run(function () { + controller.set('slugValue', 'whatever!'); + controller.send('updateSlug', controller.get('slugValue')); + + Ember.RSVP.resolve(controller.get('lastPromise')).then(function () { + expect(controller.get('model.slug')).to.equal('whatever'); + + done(); + }).catch(done); + }); + }); + + it('should set the slug if the new slug is different', function (done) { + var controller = this.subject({ + slugGenerator: Ember.Object.create({ + generateSlug: function (str) { + var promise; + promise = Ember.RSVP.resolve(str); + this.set('lastPromise', promise); + return promise; + } + }), + model: Ember.Object.create({ + slug: 'whatever', + save: Ember.K + }) + }); + + Ember.run(function () { + controller.set('slugValue', 'changed'); + controller.send('updateSlug', controller.get('slugValue')); + + Ember.RSVP.resolve(controller.get('lastPromise')).then(function () { + expect(controller.get('model.slug')).to.equal('changed'); + + done(); + }).catch(done); + }); + }); + + it('should save the post when the slug changes and the post is not new', function (done) { + var controller = this.subject({ + slugGenerator: Ember.Object.create({ + generateSlug: function (str) { + var promise; + promise = Ember.RSVP.resolve(str); + this.set('lastPromise', promise); + return promise; + } + }), + model: Ember.Object.create({ + slug: 'whatever', + saved: 0, + isNew: false, + save: function () { + this.incrementProperty('saved'); + } + }) + }); + + Ember.run(function () { + controller.set('slugValue', 'changed'); + controller.send('updateSlug', controller.get('slugValue')); + + Ember.RSVP.resolve(controller.get('lastPromise')).then(function () { + expect(controller.get('model.slug')).to.equal('changed'); + expect(controller.get('model.saved')).to.equal(1); + + done(); + }).catch(done); + }); + }); + + it('should not save the post when the slug changes and the post is new', function (done) { + var controller = this.subject({ + slugGenerator: Ember.Object.create({ + generateSlug: function (str) { + var promise; + promise = Ember.RSVP.resolve(str); + this.set('lastPromise', promise); + return promise; + } + }), + model: Ember.Object.create({ + slug: 'whatever', + saved: 0, + isNew: true, + save: function () { + this.incrementProperty('saved'); + } + }) + }); + + Ember.run(function () { + controller.set('slugValue', 'changed'); + controller.send('updateSlug', controller.get('slugValue')); + + Ember.RSVP.resolve(controller.get('lastPromise')).then(function () { + expect(controller.get('model.slug')).to.equal('changed'); + expect(controller.get('model.saved')).to.equal(0); + + done(); + }).catch(done); + }); + }); + }); + } + ); + }); +define("ghost/tests/unit/controllers/settings-general_test", + ["ember-mocha"], + function(__dependency1__) { + "use strict"; + /* jshint expr:true */ + var describeModule = __dependency1__.describeModule; + var it = __dependency1__.it; + + describeModule( + 'controller:settings/general', + + function () { + it('isDatedPermalinks should be correct', function () { + var controller = this.subject({ + model: Ember.Object.create({ + permalinks: '/:year/:month/:day/:slug/' + }) + }); + + expect(controller.get('isDatedPermalinks')).to.be.ok; + + Ember.run(function () { + controller.set('model.permalinks', '/:slug/'); + + expect(controller.get('isDatedPermalinks')).to.not.be.ok; + }); + }); + + it('setting isDatedPermalinks should switch between dated and slug', function () { + var controller = this.subject({ + model: Ember.Object.create({ + permalinks: '/:year/:month/:day/:slug/' + }) + }); + + Ember.run(function () { + controller.set('isDatedPermalinks', false); + + expect(controller.get('isDatedPermalinks')).to.not.be.ok; + expect(controller.get('model.permalinks')).to.equal('/:slug/'); + }); + + Ember.run(function () { + controller.set('isDatedPermalinks', true); + + expect(controller.get('isDatedPermalinks')).to.be.ok; + expect(controller.get('model.permalinks')).to.equal('/:year/:month/:day/:slug/'); + }); + }); + + it('themes should be correct', function () { + var controller, + themes = []; + + themes.push({ + name: 'casper', + active: true, + package: { + name: 'Casper', + version: '1.1.5' + } + }); + + themes.push({ + name: 'rasper', + package: { + name: 'Rasper', + version: '1.0.0' + } + }); + + controller = this.subject({ + model: Ember.Object.create({ + availableThemes: themes + }) + }); + + themes = controller.get('themes'); + expect(themes).to.be.an.Array; + expect(themes.length).to.equal(2); + expect(themes.objectAt(0).name).to.equal('casper'); + expect(themes.objectAt(0).active).to.be.ok; + expect(themes.objectAt(0).label).to.equal('Casper - 1.1.5'); + expect(themes.objectAt(1).name).to.equal('rasper'); + expect(themes.objectAt(1).active).to.not.be.ok; + expect(themes.objectAt(1).label).to.equal('Rasper - 1.0.0'); + }); + } + ); + }); +define("ghost/tests/unit/models/post_test", + ["ember-mocha"], + function(__dependency1__) { + "use strict"; + /* jshint expr:true */ + var describeModel = __dependency1__.describeModel; + var it = __dependency1__.it; + + describeModel('post', + { + needs:['model:user', 'model:tag', 'model:role'] + }, + + function () { + it('has a validation type of "post"', function () { + var model = this.subject(); + + expect(model.validationType).to.equal('post'); + }); + + it('isPublished and isDraft are correct', function () { + var model = this.subject({ + status: 'published' + }); + + expect(model.get('isPublished')).to.be.ok; + expect(model.get('isDraft')).to.not.be.ok; + + Ember.run(function () { + model.set('status', 'draft'); + + expect(model.get('isPublished')).to.not.be.ok; + expect(model.get('isDraft')).to.be.ok; + }); + }); + + it('isAuthoredByUser is correct', function () { + var model = this.subject({ + author_id: 15 + }), + user = Ember.Object.create({id: '15'}); + + expect(model.isAuthoredByUser(user)).to.be.ok; + + Ember.run(function () { + model.set('author_id', 1); + + expect(model.isAuthoredByUser(user)).to.not.be.ok; + }); + }); + + it('updateTags removes and deletes old tags', function () { + var model = this.subject(); + + Ember.run(this, function () { + var modelTags = model.get('tags'), + tag1 = this.store().createRecord('tag', {id: '1'}), + tag2 = this.store().createRecord('tag', {id: '2'}), + tag3 = this.store().createRecord('tag'); + + // During testing a record created without an explicit id will get + // an id of 'fixture-n' instead of null + tag3.set('id', null); + + modelTags.pushObject(tag1); + modelTags.pushObject(tag2); + modelTags.pushObject(tag3); + + expect(model.get('tags.length')).to.equal(3); + + model.updateTags(); + + expect(model.get('tags.length')).to.equal(2); + expect(model.get('tags.firstObject.id')).to.equal('1'); + expect(model.get('tags').objectAt(1).get('id')).to.equal('2'); + expect(tag1.get('isDeleted')).to.not.be.ok; + expect(tag2.get('isDeleted')).to.not.be.ok; + expect(tag3.get('isDeleted')).to.be.ok; + }); + }); + } + ); + }); +define("ghost/tests/unit/models/role_test", + ["ember-mocha"], + function(__dependency1__) { + "use strict"; + var describeModel = __dependency1__.describeModel; + var it = __dependency1__.it; + + describeModel('role', function () { + it('provides a lowercase version of the name', function () { + var model = this.subject({ + name: 'Author' + }); + + expect(model.get('name')).to.equal('Author'); + expect(model.get('lowerCaseName')).to.equal('author'); + + Ember.run(function () { + model.set('name', 'Editor'); + + expect(model.get('name')).to.equal('Editor'); + expect(model.get('lowerCaseName')).to.equal('editor'); + }); + }); + }); + }); +define("ghost/tests/unit/models/setting_test", + ["ember-mocha"], + function(__dependency1__) { + "use strict"; + var describeModel = __dependency1__.describeModel; + var it = __dependency1__.it; + + describeModel('setting', function () { + it('has a validation type of "setting"', function () { + var model = this.subject(); + + expect(model.get('validationType')).to.equal('setting'); + }); + }); + }); +define("ghost/tests/unit/models/tag_test", + ["ember-mocha"], + function(__dependency1__) { + "use strict"; + var describeModel = __dependency1__.describeModel; + var it = __dependency1__.it; + + describeModel('tag', function () { + it('has a validation type of "tag"', function () { + var model = this.subject(); + + expect(model.get('validationType')).to.equal('tag'); + }); + }); + }); +define("ghost/tests/unit/models/user_test", + ["ember-mocha"], + function(__dependency1__) { + "use strict"; + /*jshint expr:true */ + var describeModel = __dependency1__.describeModel; + var it = __dependency1__.it; + + describeModel('user', + { + needs: ['model:role'] + }, + + function () { + it('has a validation type of "user"', function () { + var model = this.subject(); + + expect(model.get('validationType')).to.equal('user'); + }); + + it('active property is correct', function () { + var model = this.subject({ + status: 'active' + }); + + expect(model.get('active')).to.be.ok; + + ['warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked'].forEach(function (status) { + Ember.run(function () { + model.set('status', status); + + expect(model.get('status')).to.be.ok; + }); + }); + + Ember.run(function () { + model.set('status', 'inactive'); + + expect(model.get('active')).to.not.be.ok; + }); + + Ember.run(function () { + model.set('status', 'invited'); + + expect(model.get('active')).to.not.be.ok; + }); + }); + + it('invited property is correct', function () { + var model = this.subject({ + status: 'invited' + }); + + expect(model.get('invited')).to.be.ok; + + Ember.run(function () { + model.set('status', 'invited-pending'); + + expect(model.get('invited')).to.be.ok; + }); + + Ember.run(function () { + model.set('status', 'active'); + + expect(model.get('invited')).to.not.be.ok; + }); + + Ember.run(function () { + model.set('status', 'inactive'); + + expect(model.get('invited')).to.not.be.ok; + }); + }); + + it('pending property is correct', function () { + var model = this.subject({ + status: 'invited-pending' + }); + + expect(model.get('pending')).to.be.ok; + + Ember.run(function () { + model.set('status', 'invited'); + + expect(model.get('pending')).to.not.be.ok; + }); + + Ember.run(function () { + model.set('status', 'inactive'); + + expect(model.get('pending')).to.not.be.ok; + }); + }); + + it('role property is correct', function () { + var model, + role; + + model = this.subject(); + + Ember.run(this, function () { + role = this.store().createRecord('role', {name: 'Author'}); + + model.get('roles').pushObject(role); + + expect(model.get('role.name')).to.equal('Author'); + }); + + Ember.run(this, function () { + role = this.store().createRecord('role', {name: 'Editor'}); + + model.set('role', role); + + expect(model.get('role.name')).to.equal('Editor'); + }); + }); + + it('isAuthor property is correct', function () { + var model = this.subject(); + + Ember.run(this, function () { + var role = this.store().createRecord('role', {name: 'Author'}); + + model.set('role', role); + + expect(model.get('isAuthor')).to.be.ok; + expect(model.get('isEditor')).to.not.be.ok; + expect(model.get('isAdmin')).to.not.be.ok; + expect(model.get('isOwner')).to.not.be.ok; + }); + }); + + it('isEditor property is correct', function () { + var model = this.subject(); + + Ember.run(this, function () { + var role = this.store().createRecord('role', {name: 'Editor'}); + + model.set('role', role); + + expect(model.get('isEditor')).to.be.ok; + expect(model.get('isAuthor')).to.not.be.ok; + expect(model.get('isAdmin')).to.not.be.ok; + expect(model.get('isOwner')).to.not.be.ok; + }); + }); + + it('isAdmin property is correct', function () { + var model = this.subject(); + + Ember.run(this, function () { + var role = this.store().createRecord('role', {name: 'Administrator'}); + + model.set('role', role); + + expect(model.get('isAdmin')).to.be.ok; + expect(model.get('isAuthor')).to.not.be.ok; + expect(model.get('isEditor')).to.not.be.ok; + expect(model.get('isOwner')).to.not.be.ok; + }); + }); + + it('isOwner property is correct', function () { + var model = this.subject(); + + Ember.run(this, function () { + var role = this.store().createRecord('role', {name: 'Owner'}); + + model.set('role', role); + + expect(model.get('isOwner')).to.be.ok; + expect(model.get('isAuthor')).to.not.be.ok; + expect(model.get('isAdmin')).to.not.be.ok; + expect(model.get('isEditor')).to.not.be.ok; + }); + }); + } + ); + }); define("ghost/tests/unit/utils/ghost-paths_test", ["ghost/utils/ghost-paths"], function(__dependency1__) { diff --git a/node_modules/ghost/core/built/scripts/ghost.min.js b/node_modules/ghost/core/built/scripts/ghost.min.js index b1e78c8a..e09b6b45 100644 --- a/node_modules/ghost/core/built/scripts/ghost.min.js +++ b/node_modules/ghost/core/built/scripts/ghost.min.js @@ -1,10 +1,14 @@ -define("ghost/adapters/application",["ghost/adapters/embedded-relation-adapter","exports"],function(a,b){"use strict";var c=a["default"],d=c.extend();b["default"]=d}),define("ghost/adapters/base",["ghost/utils/ghost-paths","exports"],function(a,b){"use strict";var c=a["default"],d=DS.RESTAdapter.extend({host:window.location.origin,namespace:c().apiRoot.slice(1),findQuery:function(a,b,c){var d;return c.id&&(d=c.id,delete c.id),this.ajax(this.buildURL(b.typeKey,d),"GET",{data:c})},buildURL:function(a,b){var c=this._super(a,b);return"/"!==c.slice(-1)&&(c+="/"),c},deleteRecord:function(){var a=this._super.apply(this,arguments);return a.then(function(){return null})}});b["default"]=d}),define("ghost/adapters/embedded-relation-adapter",["ghost/adapters/base","exports"],function(a,b){"use strict";var c=a["default"],d=c.extend({find:function(a,b,c){return this.ajax(this.buildIncludeURL(a,b,c),"GET")},findQuery:function(a,b,c){return this._super(a,b,this.buildQuery(a,b,c))},findAll:function(a,b,c){var d={};return c&&(d.since=c),this.findQuery(a,b,d)},createRecord:function(a,b,c){return this.saveRecord(a,b,c,{method:"POST"})},updateRecord:function(a,b,c){var d={method:"PUT",id:Ember.get(c,"id")};return this.saveRecord(a,b,c,d)},saveRecord:function(a,b,c,d){d=d||{};var e=this.buildIncludeURL(a,b,d.id),f=this.preparePayload(a,b,c);return this.ajax(e,d.method,f)},preparePayload:function(a,b,c){var d=a.serializerFor(b.typeKey),e={};return d.serializeIntoHash(e,b,c),{data:e}},buildIncludeURL:function(a,b,c){var d=this.buildURL(b.typeKey,c),e=this.getEmbeddedRelations(a,b);return e.length&&(d+="?include="+e.join(",")),d},buildQuery:function(a,b,c){var d=this.getEmbeddedRelations(a,b),e=c||{},f={};return d.length&&("string"==typeof c||"number"==typeof c?(e={},e.id=c,e.include=d.join(",")):("object"==typeof c||Ember.isNone(c))&&(e=e||{},d=d.concat(e.include?e.include.split(","):[]),d.forEach(function(a){f[a]=!0}),e.include=Object.keys(f).join(","))),e},getEmbeddedRelations:function(a,b){var c=a.modelFor(b),d=[];return c.eachRelationship(function(a,b){"hasMany"===b.kind&&Object.prototype.hasOwnProperty.call(b.options,"embedded")&&"always"===b.options.embedded&&d.push(a)}),d}});b["default"]=d}),define("ghost/adapters/setting",["ghost/adapters/application","exports"],function(a,b){"use strict";var c=a["default"],d=c.extend({updateRecord:function(a,b,c){var d={},e=a.serializerFor(b.typeKey);return delete c.id,e.serializeIntoHash(d,b,c),this.ajax(this.buildURL(b.typeKey),"PUT",{data:d})}});b["default"]=d}),define("ghost/adapters/user",["ghost/adapters/application","exports"],function(a,b){"use strict";var c=a["default"],d=c.extend({find:function(a,b,c){return this.findQuery(a,b,{id:c,status:"all"})}});b["default"]=d}),define("ghost/app",["ember/resolver","ember/load-initializers","ghost/utils/link-view","ghost/utils/text-field","ghost/config","exports"],function(a,b,c,d,e,f){"use strict";var g=a["default"],h=b["default"],i=e["default"];Ember.MODEL_FACTORY_INJECTIONS=!0;var j=Ember.Application.extend({modulePrefix:"ghost",Resolver:g["default"]});i(j),h(j,"ghost"),f["default"]=j}),define("ghost/assets/lib/touch-editor",["exports"],function(a){"use strict";var b=function(){var a,b=function(){};return a=function(a){this.textarea=a,this.win={document:this.textarea},this.ready=!0,this.wrapping=document.createElement("div");var b=this.textarea.parentNode;this.wrapping.appendChild(this.textarea),b.appendChild(this.wrapping),this.textarea.style.opacity=1},a.prototype={setOption:function(a,b){"onChange"===a&&$(this.textarea).change(b)},eachLine:function(){return[]},getValue:function(){return this.textarea.value},setValue:function(a){this.textarea.value=a},focus:b,getCursor:function(){return{line:0,ch:0}},setCursor:b,currentLine:function(){return 0},cursorPosition:function(){return{character:0}},addMarkdown:b,nthLine:b,refresh:b,selectLines:b,on:b,off:b},a};a["default"]=b}),define("ghost/assets/lib/uploader",["ghost/utils/ghost-paths","exports"],function(a,b){"use strict";var c,d,e=a["default"],f=e();c=function(a,b){var c='
',d='',e=$("
",{"class":"js-upload-progress progress progress-success active",role:"progressbar","aria-valuemin":"0","aria-valuemax":"100"}).append($("
",{"class":"js-upload-progress-bar bar",style:"width:0%"}));$.extend(this,{complete:function(c){function d(b,c){a.find("img.js-upload-target").attr({width:b,height:c}).css({display:"block"}),a.find(".fileupload-loading").remove(),a.css({height:"auto"}),a.delay(250).animate({opacity:100},1e3,function(){$(".js-button-accept").prop("disabled",!1),i.init()})}function g(b){a.animate({opacity:0},250,function(){a.removeClass("image-uploader").addClass("pre-image-uploader"),a.css({minHeight:0}),i.removeExtras(),a.animate({height:b.height()},250,function(){d(b.width(),b.height())})})}function h(){var d=a.find("img.js-upload-target").attr({src:"",width:"auto",height:"auto"});e.animate({opacity:0},250,function(){a.find("span.media").after(''),b.editor||e.find(".fileupload-loading").css({top:"56px"})}),a.trigger("uploadsuccess",[c]),d.one("load",function(){g(d)}).attr("src",c)}var i=this;h()},bindFileUpload:function(){var c=this;a.find(".js-fileupload").fileupload().fileupload("option",{url:f.apiRoot+"/uploads/",add:function(c,d){$(".js-button-accept").prop("disabled",!0),a.find(".js-fileupload").removeClass("right"),a.find(".js-url").remove(),e.find(".js-upload-progress-bar").removeClass("fail"),a.trigger("uploadstart",[a.attr("id")]),a.find("span.media, div.description, a.image-url, a.image-webcam").animate({opacity:0},250,function(){a.find("div.description").hide().css({opacity:100}),b.progressbar&&(a.find("div.js-fail").after(e),e.animate({opacity:100},250)),d.submit()})},dropZone:b.fileStorage?a:null,progressall:function(c,d){var f=parseInt(d.loaded/d.total*100,10);b.editor||e.find("div.js-progress").css({position:"absolute",top:"40px"}),b.progressbar&&(a.trigger("uploadprogress",[f,d]),e.find(".js-upload-progress-bar").css("width",f+"%"))},fail:function(b,d){$(".js-button-accept").prop("disabled",!1),a.trigger("uploadfailure",[d.result]),a.find(".js-upload-progress-bar").addClass("fail"),a.find("div.js-fail").text(413===d.jqXHR.status?"The image you uploaded was larger than the maximum file size your server allows.":415===d.jqXHR.status?"The image type you uploaded is not supported. Please use .PNG, .JPG, .GIF, .SVG.":"Something went wrong :("),a.find("div.js-fail, button.js-fail").fadeIn(1500),a.find("button.js-fail").on("click",function(){a.css({minHeight:0}),a.find("div.description").show(),c.removeExtras(),c.init()})},done:function(a,b){c.complete(b.result)}})},buildExtras:function(){a.find("span.media")[0]||a.prepend(''),a.find("div.description")[0]||a.append('
Add image
'),a.find("div.js-fail")[0]||a.append(''),a.find("button.js-fail")[0]||a.append(''),a.find("a.image-url")[0]||a.append('')},removeExtras:function(){a.find("span.media, div.js-upload-progress, a.image-url, a.image-upload, a.image-webcam, div.js-fail, button.js-fail, a.js-cancel").remove()},initWithDropzone:function(){var c=this;return a.find("img.js-upload-target").css({display:"none"}),a.find("div.description").show(),a.removeClass("pre-image-uploader image-uploader-url").addClass("image-uploader"),this.removeExtras(),this.buildExtras(),this.bindFileUpload(),b.fileStorage?void a.find("a.image-url").on("click",function(){c.initUrl()}):void c.initUrl()},initUrl:function(){var e,f=this;this.removeExtras(),a.addClass("image-uploader-url").removeClass("pre-image-uploader"),a.find(".js-fileupload").addClass("right"),b.fileStorage&&a.append(d),a.find(".js-cancel").on("click",function(){a.find(".js-url").remove(),a.find(".js-fileupload").removeClass("right"),a.trigger("imagecleared"),f.removeExtras(),f.initWithDropzone()}),a.find("div.description").before(c),b.editor&&a.find("div.js-url").append(''),a.find(".js-button-accept").on("click",function(){e=a.find(".js-upload-url").val(),a.find("div.description").hide(),a.find(".js-fileupload").removeClass("right"),a.find(".js-url").remove(),""===e?(a.trigger("uploadsuccess","http://"),f.initWithDropzone()):f.complete(e)}),b.fileStorage!==!1&&a.append(''),a.find("a.image-upload").on("click",function(){a.find(".js-url").remove(),a.find(".js-fileupload").removeClass("right"),f.initWithDropzone()})},initWithImage:function(){var b=this;a.removeClass("image-uploader image-uploader-url").addClass("pre-image-uploader"),a.find("div.description").hide(),a.find("img.js-upload-target").show(),a.append(d),a.find(".js-cancel").on("click",function(){a.find("img.js-upload-target").attr({src:""}),a.find("div.description").show(),a.trigger("imagecleared"),a.delay(2500).animate({opacity:100},1e3,function(){b.init()}),a.trigger("uploadsuccess","http://"),b.initWithDropzone()})},init:function(){var b=a.find("img.js-upload-target");b[0]||a.prepend(''),$(".js-button-accept").prop("disabled",!1),""===b.attr("src")||void 0===b.attr("src")?this.initWithDropzone():this.initWithImage()},reset:function(){a.find(".js-url").remove(),a.find(".js-fileupload").removeClass("right"),this.removeExtras(),this.initWithDropzone()}})},d=function(a){var b=$.extend({progressbar:!0,editor:!1,fileStorage:!0},a);return this.each(function(){var a,d=$(this);a=new c(d,b),this.uploaderUi=a,a.init()})},b["default"]=d}),define("ghost/components/gh-activating-list-item",["exports"],function(a){"use strict";var b=Ember.Component.extend({tagName:"li",classNameBindings:["active"],active:!1,unfocusLink:function(){this.$("a").blur()}.on("click")});a["default"]=b}),define("ghost/components/gh-codemirror",["ghost/mixins/marker-manager","ghost/utils/codemirror-mobile","ghost/utils/set-scroll-classname","ghost/utils/codemirror-shortcuts","exports"],function(a,b,c,d,e){"use strict";var f,g,h,i=a["default"],j=b["default"],k=c["default"],l=d["default"];l.init(),f=function(a,b){var c,d=a.component;for(c=b.from.line;c-1&&this.$("option:eq("+c+")").prop("selected",!0)},change:function(){this._changeSelection()},_changeSelection:function(){var a=this._selectedValue();Ember.set(this,"value",a),this.sendAction("onChange",a)},_selectedValue:function(){var a=this.$("select")[0].selectedIndex;return this.get("options").objectAt(a)}});a["default"]=b}),define("ghost/components/gh-tab-pane",["exports"],function(a){"use strict";var b=Ember.Component.extend({classNameBindings:["active"],tabsManager:Ember.computed(function(){return this.nearestWithProperty("isTabsManager")}),tab:Ember.computed("tabsManager.tabs.[]","tabsManager.tabPanes.[]",function(){var a=this.get("tabsManager.tabPanes").indexOf(this),b=this.get("tabsManager.tabs");return b&&b.objectAt(a)}),active:Ember.computed.alias("tab.active"),registerWithTabs:function(){this.get("tabsManager").registerTabPane(this)}.on("didInsertElement"),unregisterWithTabs:function(){this.get("tabsManager").unregisterTabPane(this)}.on("willDestroyElement")});a["default"]=b}),define("ghost/components/gh-tab",["exports"],function(a){"use strict";var b=Ember.Component.extend({tabsManager:Ember.computed(function(){return this.nearestWithProperty("isTabsManager")}),active:Ember.computed("tabsManager.activeTab",function(){return this.get("tabsManager.activeTab")===this}),index:Ember.computed("tabsManager.tabs.@each",function(){return this.get("tabsManager.tabs").indexOf(this)}),click:function(){this.get("tabsManager").select(this)},registerWithTabs:function(){this.get("tabsManager").registerTab(this)}.on("didInsertElement"),unregisterWithTabs:function(){this.get("tabsManager").unregisterTab(this)}.on("willDestroyElement")});a["default"]=b}),define("ghost/components/gh-tabs-manager",["exports"],function(a){"use strict";var b=Ember.Component.extend({activeTab:null,tabs:[],tabPanes:[],select:function(a){this.set("activeTab",a),this.sendAction("selected")},isTabsManager:!0,registerTab:function(a){this.get("tabs").addObject(a)},unregisterTab:function(a){this.get("tabs").removeObject(a)},registerTabPane:function(a){this.get("tabPanes").addObject(a)},unregisterTabPane:function(a){this.get("tabPanes").removeObject(a)}});a["default"]=b}),define("ghost/components/gh-textarea",["ghost/mixins/text-input","exports"],function(a,b){"use strict";var c=a["default"],d=Ember.TextArea.extend(c);b["default"]=d}),define("ghost/components/gh-trim-focus-input",["exports"],function(a){"use strict";var b=Ember.TextField.extend({focus:!0,attributeBindings:["autofocus"],autofocus:Ember.computed(function(){return device.ios()?!1:"autofocus"}),setFocus:function(){this.focus&&!device.ios()&&this.$().val(this.$().val()).focus()}.on("didInsertElement"),focusOut:function(){var a=this.$().val();this.$().val(a.trim())}});a["default"]=b}),define("ghost/components/gh-upload-modal",["ghost/components/gh-modal-dialog","ghost/assets/lib/uploader","exports"],function(a,b,c){"use strict";var d=a["default"],e=b["default"],f=d.extend({layoutName:"components/gh-modal-dialog",didInsertElement:function(){this._super(),e.call(this.$(".js-drop-zone"),{fileStorage:this.get("config.fileStorage")})},confirm:{reject:{func:function(){return!0},buttonClass:"btn btn-default",text:"Cancel"},accept:{buttonClass:"btn btn-blue right",text:"Save",func:function(){var a="model."+this.get("imageType");return this.$(".js-upload-url").val()?this.set(a,this.$(".js-upload-url").val()):this.set(a,this.$(".js-upload-target").attr("src")),!0}}},actions:{closeModal:function(){this.sendAction()},confirm:function(a){var b=this.get("confirm."+a+".func");"function"==typeof b&&b.apply(this),this.sendAction(),this.sendAction("confirm"+a)}}});c["default"]=f}),define("ghost/components/gh-uploader",["ghost/assets/lib/uploader","exports"],function(a,b){"use strict";var c=a["default"],d=Ember.Component.extend({classNames:["image-uploader","js-post-image-upload"],setup:function(){var a=this.$(),b=this;this.set("uploaderReference",c.call(a,{editor:!0,fileStorage:this.get("config.fileStorage")})),a.on("uploadsuccess",function(a,c){c&&""!==c&&"http://"!==c&&b.sendAction("uploaded",c)}),a.on("imagecleared",function(){b.sendAction("canceled")})}.on("didInsertElement"),removeListeners:function(){var a=this.$();a.off(),a.find(".js-cancel").off()}.on("willDestroyElement")});b["default"]=d}),define("ghost/components/gh-url-preview",["exports"],function(a){"use strict";var b=Ember.Component.extend({classNames:"ghost-url-preview",prefix:null,slug:null,theUrl:null,generateUrl:function(){var a=this.get("config").blogUrl,b=a.substr(a.indexOf("://")+3),c=this.get("prefix")?this.get("prefix")+"/":"",d=this.get("slug")?this.get("slug"):"",e=b+"/"+c+d;this.set("the-url",e)}.on("didInsertElement").observes("slug")});a["default"]=b}),define("ghost/config",["exports"],function(a){"use strict";function b(a){!a instanceof Ember.Application}a["default"]=b}),define("ghost/controllers/application",["exports"],function(a){"use strict";var b=Ember.Controller.extend({hideNav:Ember.computed.match("currentPath",/(error|signin|signup|setup|forgotten|reset)/),topNotificationCount:0,showGlobalMobileNav:!1,showSettingsMenu:!1,userImageAlt:Ember.computed("session.user.name",function(){var a=this.get("session.user.name");return a+"'s profile picture"}),actions:{topNotificationChange:function(a){this.set("topNotificationCount",a)}}});a["default"]=b}),define("ghost/controllers/editor/edit",["ghost/mixins/editor-base-controller","exports"],function(a,b){"use strict";var c=a["default"],d=Ember.Controller.extend(c);b["default"]=d}),define("ghost/controllers/editor/new",["ghost/mixins/editor-base-controller","exports"],function(a,b){"use strict";var c=a["default"],d=Ember.Controller.extend(c,{actions:{save:function(a){var b=this;return this._super(a).then(function(a){a.get("id")&&b.replaceRoute("editor.edit",a)})}}});b["default"]=d}),define("ghost/controllers/error",["exports"],function(a){"use strict";var b=Ember.Controller.extend({code:Ember.computed("content.status",function(){return this.get("content.status")>200?this.get("content.status"):500}),message:Ember.computed("content.statusText",function(){return 404===this.get("code")?"No Ghost Found":"error"!==this.get("content.statusText")?this.get("content.statusText"):"Internal Server Error"}),stack:!1});a["default"]=b}),define("ghost/controllers/feature",["exports"],function(a){"use strict";var b=Ember.Controller.extend(Ember.PromiseProxyMixin,{init:function(){var a;a=this.store.find("setting",{type:"blog,theme"}).then(function(a){return a.get("firstObject")}),this.set("promise",a)},setting:Ember.computed.alias("content"),labs:Ember.computed("isSettled","setting.labs",function(){var a={};if(this.get("isFulfilled"))try{a=JSON.parse(this.get("setting.labs")||{})}catch(b){a={}}return a}),codeInjectionUI:Ember.computed("config.codeInjectionUI","labs.codeInjectionUI",function(){return this.get("config.codeInjectionUI")||this.get("labs.codeInjectionUI")})});a["default"]=b}),define("ghost/controllers/forgotten",["ghost/utils/ajax","ghost/mixins/validation-engine","exports"],function(a,b,c){"use strict";var d=a["default"],e=b["default"],f=Ember.Controller.extend(e,{email:"",submitting:!1,validationType:"forgotten",actions:{submit:function(){var a=this,b=a.getProperties("email");this.toggleProperty("submitting"),this.validate({format:!1}).then(function(){d({url:a.get("ghostPaths.url").api("authentication","passwordreset"),type:"POST",data:{passwordreset:[{email:b.email}]}}).then(function(){a.toggleProperty("submitting"),a.notifications.showSuccess("Please check your email for instructions.",{delayed:!0}),a.set("email",""),a.transitionToRoute("signin")})["catch"](function(b){a.toggleProperty("submitting"),a.notifications.showAPIError(b,{defaultErrorText:"There was a problem with the reset, please try again."})})})["catch"](function(b){a.toggleProperty("submitting"),a.notifications.showErrors(b)})}}});c["default"]=f}),define("ghost/controllers/modals/copy-html",["exports"],function(a){"use strict";var b=Ember.Controller.extend({generatedHTML:Ember.computed.alias("model.generatedHTML")});a["default"]=b}),define("ghost/controllers/modals/delete-all",["exports"],function(a){"use strict";var b=Ember.Controller.extend({actions:{confirmAccept:function(){var a=this;ic.ajax.request(this.get("ghostPaths.url").api("db"),{type:"DELETE"}).then(function(){a.notifications.showSuccess("All content deleted from database."),a.store.unloadAll("post"),a.store.unloadAll("tag")})["catch"](function(b){a.notifications.showErrors(b)})},confirmReject:function(){return!1}},confirm:{accept:{text:"Delete",buttonClass:"btn btn-red"},reject:{text:"Cancel",buttonClass:"btn btn-default btn-minor"}}});a["default"]=b}),define("ghost/controllers/modals/delete-post",["exports"],function(a){"use strict";var b=Ember.Controller.extend({actions:{confirmAccept:function(){var a=this,b=this.get("model");b.updateTags(),b.destroyRecord().then(function(){a.get("dropdown").closeDropdowns(),a.transitionToRoute("posts.index"),a.notifications.showSuccess("Your post has been deleted.",{delayed:!0})},function(){a.notifications.showError("Your post could not be deleted. Please try again.")})},confirmReject:function(){return!1}},confirm:{accept:{text:"Delete",buttonClass:"btn btn-red"},reject:{text:"Cancel",buttonClass:"btn btn-default btn-minor"}}});a["default"]=b}),define("ghost/controllers/modals/delete-tag",["exports"],function(a){"use strict";var b=Ember.Controller.extend({postInflection:Ember.computed("model.post_count",function(){return this.get("model.post_count")>1?"posts":"post"}),actions:{confirmAccept:function(){var a=this.get("model"),b=a.get("name"),c=this;this.send("closeSettingsMenu"),a.destroyRecord().then(function(){c.notifications.showSuccess("Deleted "+b)})["catch"](function(a){c.notifications.showAPIError(a)})},confirmReject:function(){return!1}},confirm:{accept:{text:"Delete",buttonClass:"btn btn-red"},reject:{text:"Cancel",buttonClass:"btn btn-default btn-minor"}}});a["default"]=b}),define("ghost/controllers/modals/delete-user",["exports"],function(a){"use strict";var b=Ember.Controller.extend({userPostCount:Ember.computed("model.id",function(){var a,b={author:this.get("model.slug"),status:"all"};return a=this.store.find("post",b).then(function(a){return a.meta.pagination.total}),Ember.Object.extend(Ember.PromiseProxyMixin,{count:Ember.computed.alias("content"),inflection:Ember.computed("count",function(){return this.get("count")>1?"posts":"post"})}).create({promise:a})}),actions:{confirmAccept:function(){var a=this,b=this.get("model");b.destroyRecord().then(function(){a.store.unloadAll("post"),a.transitionToRoute("settings.users"),a.notifications.showSuccess("The user has been deleted.",{delayed:!0})},function(){a.notifications.showError("The user could not be deleted. Please try again.")})},confirmReject:function(){return!1}},confirm:{accept:{text:"Delete User",buttonClass:"btn btn-red"},reject:{text:"Cancel",buttonClass:"btn btn-default btn-minor"}}});a["default"]=b}),define("ghost/controllers/modals/invite-new-user",["exports"],function(a){"use strict";var b=Ember.Controller.extend({authorRole:Ember.computed(function(){var a=this;return this.store.find("role").then(function(b){var c=b.findBy("name","Author");return a.set("role",c),a.set("authorRole",c),c})}),confirm:{accept:{text:"send invitation now"},reject:{buttonClass:"hidden"}},actions:{setRole:function(a){this.set("role",a)},confirmAccept:function(){var a,b=this.get("email"),c=this.get("role"),d=this;d.set("email",""),d.set("role",d.get("authorRole")),d.send("closeModal"),this.store.find("user").then(function(e){var f=e.findBy("email",b);f?d.notifications.showWarn("invited"===f.get("status")||"invited-pending"===f.get("status")?"A user with that email address was already invited.":"A user with that email address already exists."):(a=d.store.createRecord("user",{email:b,status:"invited",role:c}),a.save().then(function(){var c="Invitation sent! ("+b+")"; -"invited-pending"===a.get("status")?d.notifications.showWarn("Invitation email was not sent. Please try resending."):d.notifications.showSuccess(c)})["catch"](function(b){a.deleteRecord(),d.notifications.showErrors(b)}))})},confirmReject:function(){return!1}}});a["default"]=b}),define("ghost/controllers/modals/leave-editor",["exports"],function(a){"use strict";var b=Ember.Controller.extend({args:Ember.computed.alias("model"),actions:{confirmAccept:function(){var a,b,c,d=this.get("args");return Ember.isArray(d)&&(a=d[0],c=d[1],b=a.get("model")),c&&a?(b.updateTags(),b.get("isNew")?b.deleteRecord():b.rollback(),a.set("isDirty",!1),window.onbeforeunload=null,void c.retry()):(this.notifications.showError("Sorry, there was an error in the application. Please let the Ghost team know what happened."),!0)},confirmReject:function(){}},confirm:{accept:{text:"Leave",buttonClass:"btn btn-red"},reject:{text:"Stay",buttonClass:"btn btn-default btn-minor"}}});a["default"]=b}),define("ghost/controllers/modals/signin",["ghost/controllers/signin","exports"],function(a,b){"use strict";var c=a["default"];b["default"]=c.extend({needs:"application",identification:Ember.computed("session.user.email",function(){return this.get("session.user.email")}),actions:{authenticate:function(){var a=this.get("controllers.application"),b=this;a.set("skipAuthSuccessHandler",!0),this._super().then(function(){b.send("closeModal"),b.notifications.showSuccess("Login successful."),b.set("password","")})["finally"](function(){a.set("skipAuthSuccessHandler",void 0)})},confirmAccept:function(){this.send("validateAndAuthenticate")}}})}),define("ghost/controllers/modals/transfer-owner",["exports"],function(a){"use strict";var b=Ember.Controller.extend({actions:{confirmAccept:function(){var a=this.get("model"),b=this.get("ghostPaths.url").api("users","owner"),c=this;c.get("dropdown").closeDropdowns(),ic.ajax.request(b,{type:"PUT",data:{owner:[{id:a.get("id")}]}}).then(function(b){b&&Ember.isArray(b.users)&&b.users.forEach(function(a){var b=c.store.getById("user",a.id),d=c.store.getById("role",a.roles[0].id);b.set("role",d)}),c.notifications.showSuccess("Ownership successfully transferred to "+a.get("name"))})["catch"](function(a){c.notifications.showAPIError(a)})},confirmReject:function(){return!1}},confirm:{accept:{text:"Yep - I'm sure",buttonClass:"btn btn-red"},reject:{text:"Cancel",buttonClass:"btn btn-default btn-minor"}}});a["default"]=b}),define("ghost/controllers/modals/upload",["exports"],function(a){"use strict";var b=Ember.Controller.extend({acceptEncoding:"image/*",actions:{confirmAccept:function(){var a=this;this.get("model").save().then(function(b){return a.notifications.showSuccess("Saved"),b})["catch"](function(b){a.notifications.showErrors(b)})},confirmReject:function(){return!1}}});a["default"]=b}),define("ghost/controllers/post-settings-menu",["ghost/utils/date-formatting","ghost/mixins/settings-menu-controller","ghost/models/slug-generator","ghost/utils/bound-one-way","ghost/utils/isNumber","exports"],function(a,b,c,d,e,f){"use strict";var g=a.parseDateString,h=a.formatDate,i=b["default"],j=c["default"],k=d["default"],l=e["default"],m=Ember.Controller.extend(i,{debounceId:null,lastPromise:null,selectedAuthor:null,uploaderReference:null,initializeSelectedAuthor:function(){var a=this;return this.get("model.author").then(function(b){return a.set("selectedAuthor",b),b})}.observes("model"),changeAuthor:function(){var a=this.get("model.author"),b=this.get("selectedAuthor"),c=this.get("model"),d=this;b.get("id")!==a.get("id")&&(c.set("author",b),this.get("model.isNew")||c.save()["catch"](function(b){d.showErrors(b),d.set("selectedAuthor",a),c.rollback()}))}.observes("selectedAuthor"),authors:Ember.computed(function(){var a={};return a.promise=this.store.find("user",{limit:"all"}).then(function(a){return a.rejectBy("id","me").sortBy("name")}).then(function(a){return a.filter(function(a){return a.get("active")})}),Ember.ArrayProxy.extend(Ember.PromiseProxyMixin).create(a)}),publishedAtValue:Ember.computed("model.published_at",function(){var a=this.get("model.published_at");return h(arguments.length>1?moment():a?a:moment())}),slugValue:k("model.slug"),slugGenerator:Ember.computed(function(){return j.create({ghostPaths:this.get("ghostPaths"),slugType:"post"})}),generateAndSetSlug:function(a){var b,c=this,d=this.get("model.titleScratch"),e=this.get("lastPromise");"(Untitled)"===d&&this.get("model.slug")||(b=Ember.RSVP.resolve(e).then(function(){return c.get("slugGenerator").generateSlug(d).then(function(b){c.set(a,b)})["catch"](function(){})}),this.set("lastPromise",b))},metaTitleScratch:k("model.meta_title"),metaDescriptionScratch:k("model.meta_description"),seoTitle:Ember.computed("model.titleScratch","metaTitleScratch",function(){var a=this.get("metaTitleScratch")||"";return a=a.length>0?a:this.get("model.titleScratch"),a.length>70&&(a=a.substring(0,70).trim(),a=Ember.Handlebars.Utils.escapeExpression(a),a=new Ember.Handlebars.SafeString(a+"…")),a}),seoDescription:Ember.computed("model.scratch","metaDescriptionScratch",function(){var a,b,c=this.get("metaDescriptionScratch")||"",d="";return c.length>0?b=c:(a=$(".rendered-markdown"),void 0!==a&&a.length>0&&(d=a.clone(),d.find(".js-drop-zone").remove(),d=d[0].innerHTML),b=$("
",{html:d}).text(),b=b.replace(/\n+/g," ").trim()),b.length>156&&(b=b.substring(0,156).trim(),b=Ember.Handlebars.Utils.escapeExpression(b),b=new Ember.Handlebars.SafeString(b+"…")),b}),seoURL:Ember.computed("model.slug",function(){var a=this.get("config").blogUrl,b=this.get("model.slug")?this.get("model.slug"):"",c=a+"/"+b;return b&&(c+="/"),c.length>70&&(c=c.substring(0,70).trim(),c=new Ember.Handlebars.SafeString(c+"…")),c}),addTitleObserver:function(){(this.get("model.isNew")||"(Untitled)"===this.get("model.title"))&&this.addObserver("model.titleScratch",this,"titleObserver")}.observes("model"),titleObserver:function(){var a,b=this.get("model.title");(this.get("model.isNew")&&!b||"(Untitled)"===b)&&(a=Ember.run.debounce(this,"generateAndSetSlug",["slug"],700)),this.set("debounceId",a)},showErrors:function(a){a=Ember.isArray(a)?a:[a],this.notifications.showErrors(a)},showSuccess:function(a){this.notifications.showSuccess(a)},actions:{togglePage:function(){var a=this;this.toggleProperty("model.page"),this.get("model.isNew")||this.get("model").save()["catch"](function(b){a.showErrors(b),a.get("model").rollback()})},toggleFeatured:function(){var a=this;this.toggleProperty("model.featured"),this.get("model.isNew")||this.get("model").save(this.get("saveOptions"))["catch"](function(b){a.showErrors(b),a.get("model").rollback()})},updateSlug:function(a){var b=this.get("model.slug"),c=this;return a=a||b,a=a&&a.trim(),a&&b!==a?void this.get("slugGenerator").generateSlug(a).then(function(d){if(d!==b){var e=d.split("-"),f=Number(e.pop());if(l(f)&&f>0&&b===e.join("-")&&d!==a)return void c.set("slugValue",b);if(c.set("model.slug",d),c.hasObserverFor("model.titleScratch")&&c.removeObserver("model.titleScratch",c,"titleObserver"),!c.get("model.isNew"))return c.get("model").save()}})["catch"](function(a){c.showErrors(a),c.get("model").rollback()}):void this.set("slugValue",b)},setPublishedAt:function(a){var b="",c=g(a),d=this.get("model.published_at"),e=this;return a?(c.isValid()||(b="Published Date must be a valid date with format: DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)"),c.diff(new Date,"h")>0&&(b="Published Date cannot currently be in the future."),b?void this.showErrors(b):void(d&&d.isSame(c)||(this.set("model.published_at",c),this.get("model.isNew")||this.get("model").save()["catch"](function(a){e.showErrors(a),e.get("model").rollback()})))):void(this.get("model.isDraft")&&this.set("model.published_at",null))},setMetaTitle:function(a){var b=this,c=this.get("model.meta_title")||"";c!==a&&(this.set("model.meta_title",a),this.get("model.isNew")||this.get("model").save()["catch"](function(a){b.showErrors(a)}))},setMetaDescription:function(a){var b=this,c=this.get("model.meta_description")||"";c!==a&&(this.set("model.meta_description",a),this.get("model.isNew")||this.get("model").save()["catch"](function(a){b.showErrors(a)}))},setCoverImage:function(a){var b=this;this.set("model.image",a),this.get("model.isNew")||this.get("model").save()["catch"](function(a){b.showErrors(a),b.get("model").rollback()})},clearCoverImage:function(){var a=this;this.set("model.image",""),this.get("model.isNew")||this.get("model").save()["catch"](function(b){a.showErrors(b),a.get("model").rollback()})},resetUploader:function(){var a=this.get("uploaderReference");a&&a[0]&&a[0].uploaderUi.reset()},resetPubDate:function(){this.set("publishedAtValue","")}}});f["default"]=m}),define("ghost/controllers/post-tags-input",["exports"],function(a){"use strict";var b=Ember.Controller.extend({tagEnteredOrder:Ember.A(),tags:Ember.computed("parentController.model.tags",function(){var a=Ember.ArrayProxy.create({content:this.get("parentController.model.tags")}),b=a.get("arrangedContent").slice();return a.get("arrangedContent").clear(),this.get("tagEnteredOrder").forEach(function(c){var d=b.find(function(a){return a.get("name")===c});d&&(a.get("arrangedContent").addObject(d),b.removeObject(d))}),a.get("arrangedContent").unshiftObjects(b),a}),suggestions:null,newTagText:null,actions:{loadAllTags:function(){this.store.find("tag",{limit:"all"})},addNewTag:function(){var a,b,c,d=this.get("newTagText");return Ember.isEmpty(d)||this.hasTag(d)?void this.send("reset"):(d=d.trim(),a=d.toLowerCase(),b=this.store.all("tag").filter(function(b){return b.get("isNew")?!1:b.get("name").toLowerCase()===a}),b.get("length")?this.send("addTag",b.get("firstObject")):(c=this.store.createRecord("tag"),c.set("name",d),this.send("addTag",c)),void this.send("reset"))},addTag:function(a){Ember.isEmpty(a)||(this.get("tags").addObject(a),this.get("tagEnteredOrder").addObject(a.get("name"))),this.send("reset")},deleteTag:function(a){a&&(this.get("tags").removeObject(a),this.get("tagEnteredOrder").removeObject(a.get("name")))},deleteLastTag:function(){this.send("deleteTag",this.get("tags.lastObject"))},selectSuggestion:function(a){Ember.isEmpty(a)||(this.get("suggestions").setEach("selected",!1),a.set("selected",!0))},selectNextSuggestion:function(){var a,b,c=this.get("suggestions"),d=this.get("selectedSuggestion");Ember.isEmpty(c)||(a=c.indexOf(d),a+1=0?(c=d[a-1],this.send("selectSuggestion",c)):d.setEach("selected",!1))},addSelectedSuggestion:function(){var a=this.get("selectedSuggestion");Ember.isEmpty(a)||this.send("addTag",a.get("tag"))},reset:function(){this.set("suggestions",null),this.set("newTagText",null)}},selectedSuggestion:Ember.computed("suggestions.@each.selected",function(){var a=this.get("suggestions");return a&&a.get("length")?a.filterBy("selected").get("firstObject"):null}),updateSuggestionsList:function(){var a,b=this.get("newTagText"),c=5,d=Ember.A();return!b||Ember.isEmpty(b.trim())?void this.set("suggestions",null):(b=b.trim(),a=this.findMatchingTags(b),a=a.slice(0,c),a.forEach(function(a){var c=this.makeSuggestionObject(a,b);d.pushObject(c)},this),void this.set("suggestions",d))}.observes("newTagText"),findMatchingTags:function(a){var b,c=this,d=this.store.all("tag").filterBy("isNew",!1),e={};return 0===d.get("length")?[]:(a=a.toLowerCase(),b=d.filter(function(b){var d,f,g=b.get("name");return d=-1!==g.toLowerCase().indexOf(a),f=c.hasTag(g),d&&!f&&("undefined"==typeof e[g]?e[g]=1:e[g]+=1),1===e[g]}))},hasTag:function(a){return this.get("tags").mapBy("name").contains(a)},makeSuggestionObject:function(a,b){var c,d=Ember.Handlebars.Utils.escapeExpression(b),e=d.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&"),f=Ember.Handlebars.Utils.escapeExpression(a.get("name")),g=new RegExp("("+e+")","gi"),h=Ember.Object.create();return c=f.replace(g,"$1"),c=new Ember.Handlebars.SafeString(c),h.set("tag",a),h.set("highlightedName",c),h}});a["default"]=b}),define("ghost/controllers/posts",["ghost/mixins/pagination-controller","exports"],function(a,b){"use strict";function c(a,b){var c=a.get("published_at"),d=b.get("published_at");return c||d?!c&&d?-1:!d&&c?1:Ember.compare(c.valueOf(),d.valueOf()):0}var d=a["default"],e=Ember.ArrayController.extend(d,{postListFocused:Ember.computed.equal("keyboardFocus","postList"),postContentFocused:Ember.computed.equal("keyboardFocus","postContent"),sortProperties:["status","published_at","updated_at"],orderBy:function(a,b){var d,e,f,g,h=a.get("updated_at"),i=b.get("updated_at");return a.get("isNew")||!h?-1:b.get("isNew")||!i?1:(d=Ember.compare(parseInt(a.get("id")),parseInt(b.get("id"))),e=Ember.compare(a.get("status"),b.get("status")),f=Ember.compare(h.valueOf(),i.valueOf()),g=c(a,b),0===e?0===g?0===f?-1*d:-1*f:-1*g:e)},init:function(){this._super({modelType:"post"})}});b["default"]=e}),define("ghost/controllers/posts/post",["exports"],function(a){"use strict";var b=Ember.Controller.extend({isPublished:Ember.computed.equal("model.status","published"),classNameBindings:["model.featured"],actions:{toggleFeatured:function(){var a={disableNProgress:!0},b=this;this.toggleProperty("model.featured"),this.get("model").save(a)["catch"](function(a){b.notifications.showErrors(a)})},showPostContent:function(){this.transitionToRoute("posts.post",this.get("model"))}}});a["default"]=b}),define("ghost/controllers/reset",["ghost/utils/ajax","ghost/mixins/validation-engine","exports"],function(a,b,c){"use strict";var d=a["default"],e=b["default"],f=Ember.Controller.extend(e,{newPassword:"",ne2Password:"",token:"",submitButtonDisabled:!1,validationType:"reset",email:Ember.computed("token",function(){return atob(this.get("token")).split("|")[1]}),clearData:function(){this.setProperties({newPassword:"",ne2Password:"",token:""})},actions:{submit:function(){var a=this.getProperties("newPassword","ne2Password","token"),b=this;this.toggleProperty("submitting"),this.validate({format:!1}).then(function(){d({url:b.get("ghostPaths.url").api("authentication","passwordreset"),type:"PUT",data:{passwordreset:[a]}}).then(function(c){b.toggleProperty("submitting"),b.notifications.showSuccess(c.passwordreset[0].message,!0),b.get("session").authenticate("simple-auth-authenticator:oauth2-password-grant",{identification:b.get("email"),password:a.newPassword})})["catch"](function(a){b.notifications.showAPIError(a),b.toggleProperty("submitting")})})["catch"](function(a){b.toggleProperty("submitting"),b.notifications.showErrors(a)})}}});c["default"]=f}),define("ghost/controllers/settings",["exports"],function(a){"use strict";var b=Ember.Controller.extend({needs:["feature"],showGeneral:Ember.computed("session.user.name",function(){return this.get("session.user.isAuthor")||this.get("session.user.isEditor")?!1:!0}),showUsers:Ember.computed("session.user.name",function(){return this.get("session.user.isAuthor")?!1:!0}),showTags:Ember.computed("session.user.name",function(){return this.get("session.user.isAuthor")?!1:!0}),showCodeInjection:Ember.computed("session.user.name","controllers.feature.codeInjectionUI",function(){return this.get("session.user.isAuthor")||this.get("session.user.isEditor")||!this.get("controllers.feature.codeInjectionUI")?!1:!0}),showLabs:Ember.computed("session.user.name",function(){return this.get("session.user.isAuthor")||this.get("session.user.isEditor")?!1:!0}),showAbout:Ember.computed("session.user.name",function(){return this.get("session.user.isAuthor")?!1:!0})});a["default"]=b}),define("ghost/controllers/settings/app",["exports"],function(a){"use strict";var b,c;b={active:"active",working:"working",inactive:"inactive"},c=Ember.Controller.extend({appState:b.active,buttonText:"",setAppState:function(){this.set("appState",this.get("active")?b.active:b.inactive)}.on("init"),buttonTextSetter:function(){switch(this.get("appState")){case b.active:this.set("buttonText","Deactivate");break;case b.inactive:this.set("buttonText","Activate");break;case b.working:this.set("buttonText","Working")}}.observes("appState").on("init"),activeClass:Ember.computed("appState",function(){return this.appState===b.active?!0:!1}),inactiveClass:Ember.computed("appState",function(){return this.appState===b.inactive?!0:!1}),actions:{toggleApp:function(a){var c=this;this.set("appState",b.working),a.set("active",!a.get("active")),a.save().then(function(){c.setAppState()}).then(function(){alert("@TODO: Success")})["catch"](function(){alert("@TODO: Failure")})}}}),a["default"]=c}),define("ghost/controllers/settings/code-injection",["exports"],function(a){"use strict";var b=Ember.Controller.extend({actions:{save:function(){var a=this;return this.get("model").save().then(function(b){return a.notifications.closePassive(),a.notifications.showSuccess("Settings successfully saved."),b})["catch"](function(b){a.notifications.closePassive(),a.notifications.showErrors(b)})}}});a["default"]=b}),define("ghost/controllers/settings/general",["exports"],function(a){"use strict";var b=Ember.Controller.extend({selectedTheme:null,isDatedPermalinks:Ember.computed("model.permalinks",function(a,b){arguments.length>1&&this.set("model.permalinks",b?"/:year/:month/:day/:slug/":"/:slug/");var c=this.get("model.permalinks");return"/:slug/"!==c}),themes:Ember.computed(function(){return this.get("model.availableThemes").reduce(function(a,b){var c={};return c.name=b.name,c.label=b["package"]?b["package"].name+" - "+b["package"].version:b.name,c["package"]=b["package"],c.active=!!b.active,a.push(c),a},[])}).readOnly(),actions:{save:function(){var a=this;return this.get("model").save().then(function(b){return a.notifications.showSuccess("Settings successfully saved."),b})["catch"](function(b){a.notifications.showErrors(b)})},checkPostsPerPage:function(){var a=this.get("model.postsPerPage");(1>a||a>1e3||isNaN(a))&&this.set("model.postsPerPage",5)}}});a["default"]=b}),define("ghost/controllers/settings/labs",["exports"],function(a){"use strict";var b=Ember.Controller.extend(Ember.Evented,{needs:["feature"],uploadButtonText:"Import",importErrors:"",labsJSON:Ember.computed("model.labs",function(){return JSON.parse(this.get("model.labs")||{})}),saveLabs:function(a,b){var c=this,d=this.get("labsJSON");d[a]=b,this.set("model.labs",JSON.stringify(d)),this.get("model").save()["catch"](function(a){c.showErrors(a),c.get("model").rollback()})},codeUIFlag:Ember.computed.alias("config.codeInjectionUI"),useCodeInjectionUI:Ember.computed("controllers.feature.codeInjectionUI",function(a,b){return arguments.length>1&&this.saveLabs("codeInjectionUI",b),this.get("controllers.feature.codeInjectionUI")||!1}),actions:{onUpload:function(a){var b=this,c=new FormData;this.set("uploadButtonText","Importing"),this.set("importErrors",""),this.notifications.closePassive(),c.append("importfile",a),ic.ajax.request(this.get("ghostPaths.url").api("db"),{type:"POST",data:c,dataType:"json",cache:!1,contentType:!1,processData:!1}).then(function(){b.store.unloadAll("post"),b.store.unloadAll("tag"),b.store.unloadAll("user"),b.store.unloadAll("role"),b.store.unloadAll("setting"),b.store.unloadAll("notification"),b.notifications.showSuccess("Import successful.")})["catch"](function(a){a&&a.jqXHR&&a.jqXHR.responseJSON&&a.jqXHR.responseJSON.errors&&b.set("importErrors",a.jqXHR.responseJSON.errors),b.notifications.showError("Import Failed")})["finally"](function(){b.set("uploadButtonText","Import"),b.trigger("reset")})},exportData:function(){var a=$("#iframeDownload"),b=this.get("ghostPaths.url").api("db")+"?access_token="+this.get("session.access_token");0===a.length&&(a=$("