Added image upload reusable plugin

issue #40 and issue #280

- Adds uploader jquery plugin
- includes settings for enabling/disabling upload progress bar
- adds routing for image uploads
- adds directories by year and month based on upload date
- Implements plugin on settings - general pane
- Implements plugin on editor
- adjusted general tab to save uploaded image src

TODO:
- Add error handling
- Storing information on editor
- Add events
This commit is contained in:
cobbspur 2013-08-05 18:31:29 +01:00
parent b9dfcc485c
commit 558c9d6caa
16 changed files with 2341 additions and 11 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,177 @@
/*global jQuery, Ghost, document, Image, window */
(function ($) {
"use strict";
var UploadUi,
$loader = '<span class="media"><span class="hidden">Image Upload</span></span>' +
'<div class="description">Add image</div>' +
'<a class="image-url" title="Add image from URL"><span class="hidden">URL</span></a>' +
'<a class="image-webcam" title="Add image from webcam">' +
'<span class="hidden">Webcam</span></a>',
$progress = $('<div />', {
"class" : "js-upload-progress progress progress-success active",
"style": "opacity:0",
"role": "progressbar",
"aria-valuemin": "0",
"aria-valuemax": "100"
}).append($("<div />", {
"class": "js-upload-progress-bar bar",
"style": "width:0%"
}));
UploadUi = function ($dropzone, settings) {
var source,
$link = $('<a class="js-edit-image image-edit" href="#" >' +
'<img src="/public/assets/img/addImage.png" width="16" height="16" alt="add, edit"></a>'),
$back = $('<a class="js-return-image image-edit" href="#" >' +
'<img src="/public/assets/img/returnImage.png" width="16" height="16" alt="add, edit"></a>');
$.extend(this, {
bindFileUpload: function () {
var self = this;
$dropzone.find('.js-fileupload').fileupload().fileupload("option", {
url: '/ghost/upload',
add: function (e, data) {
$dropzone.find('a.js-return-image').remove();
$dropzone.find('span.media, div.description, a.image-url, a.image-webcam')
.animate({opacity: 0}, 250, function () {
if (settings.progressbar) {
$dropzone.find('span.media').after($progress);
$progress.animate({opacity: 100}, 250);
}
data.submit();
});
},
dropZone: $dropzone,
progressall: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
if (!settings.editor) {$progress.find('div.js-progress').css({"position": "absolute", "top": "40px"}); }
if (settings.progressbar) {
$progress.find('.js-upload-progress-bar').css('width', progress + '%');
if (data.loaded / data.total === 1) {
$progress.animate({opacity: 0}, 250, function () {
$dropzone.find('span.media').after('<img class="fileupload-loading" src="/public/img/loadingcat.gif" />');
if (!settings.editor) {$progress.find('.fileupload-loading').css({"top": "56px"}); }
});
}
}
},
done: function (e, data) {
function showImage(width, height) {
$dropzone.find('img.js-upload-target').attr({"width": width, "height": height}).css({"display": "block"});
$dropzone.find('.fileupload-loading').removeClass('fileupload-loading');
$dropzone.css({"height": "auto"});
if (!$dropzone.find('a.js-edit-image')[0]) {
$link.css({"opacity": 100});
$dropzone.find('.js-upload-target').after($link);
}
$dropzone.delay(250).animate({opacity: 100}, 1000, function () {
self.init();
});
}
function animateDropzone($img) {
$dropzone.animate({opacity: 0}, 250, function () {
$dropzone.removeClass('image-uploader').addClass('pre-image-uploader');
$dropzone.css({minHeight: 0});
self.removeExtras();
$dropzone.animate({height: $img.height()}, 250, function () {
showImage($img.width(), $img.height());
});
});
}
function preloadImage() {
var $img = $dropzone.find('img.js-upload-target')
.attr({'src': '', "width": 'auto', "height": 'auto'});
$img.one('load', function () { animateDropzone($img); })
.attr('src', data.result);
}
preloadImage();
}
});
},
removeExtras: function () {
$dropzone.find('div.description, span.media, div.js-upload-progress, a.image-url, a.image-webcam')
.remove();
},
initWithDropzone: function () {
var self = this;
//This is the start point if no image exists
$dropzone.find('img.js-upload-target').css({"display": "none"});
$dropzone.removeClass('pre-image-uploader').addClass('image-uploader');
if (!$dropzone.find('span.media')[0]) {
$dropzone.append($loader);
}
if ($dropzone.find('a.js-edit-image')[0]) {
$dropzone.find('a.js-edit-image').remove();
}
$back.on('click', function () {
$dropzone.find('a.js-return-image').remove();
$dropzone.find('img.js-upload-target').attr({"src": source}).css({"display": "block"});
self.removeExtras();
$dropzone.removeClass('image-uploader').addClass('pre-image-uploader');
self.init();
});
this.bindFileUpload();
},
initWithImage: function () {
var self = this;
// This is the start point if an image already exists
source = $dropzone.find('img.js-upload-target').attr('src');
$dropzone.removeClass('image-uploader').addClass('pre-image-uploader');
if (!$dropzone.find('a.js-edit-image')[0]) {
$link.css({"opacity": 100});
$dropzone.find('.js-upload-target').after($link);
}
$link.on('click', function () {
$dropzone.find('a.js-edit-image').remove();
$dropzone.find('img.js-upload-target').attr({"src": ""}).css({"display": "none"});
$back.css({"cursor": "pointer", "z-index": 9999, "opacity": 100});
$dropzone.find('.js-upload-target').after($back);
self.init();
});
},
init: function () {
var img;
// First check if field image is defined by checking for js-upload-target class
if ($dropzone.find('img.js-upload-target')[0]) {
if ($dropzone.find('img.js-upload-target').attr('src') === '') {
this.initWithDropzone();
} else {
this.initWithImage();
}
} else {
// This ensures there is an image we can hook into to display uploaded image
$dropzone.prepend('<img class="js-upload-target" style="display: none" src="" />');
this.init();
}
}
});
};
$.fn.upload = function (options) {
var settings = $.extend({
progressbar: true,
editor: false
}, options);
return this.each(function () {
var $dropzone = $(this),
ui;
ui = new UploadUi($dropzone, settings);
ui.init();
});
};
}(jQuery));

View File

@ -117,7 +117,7 @@
padding: 5px;
&:hover {
@include icon($i-question, '', $brown)
@include icon($i-question, '', $brown);
}
}
@ -255,7 +255,6 @@
@include breakpoint($netbook) {padding-top: 20px;}
@include breakpoint($mobile) {padding: 15px;}
}
}
// Special case, when scrolling, add shadows to content headers.
@ -331,11 +330,21 @@
text-align: center;
}
}
a {
&.image-edit {
width: 16px;
height: 16px;
}
}
img {
width: 100%;
height: auto;
}
// prevent uploaded image from being streched in editor
.pre-image-uploader img {
width: auto;
}
}

View File

@ -1135,6 +1135,16 @@ main {
color: $brown;
text-decoration: none;
}
.image-edit {
line-height: 12px;
padding: 10px;
display: block;
position: absolute;
top: 0;
left: 0;
opacity: 0;
text-decoration: none;
}
.image-webcam {
@include icon($i-camera, 12px);
@ -1146,9 +1156,10 @@ main {
right: 0;
color: $brown;
text-decoration: none;
}
.fileupload {
input {
position: absolute;
right: 0;
margin: 0;
@ -1162,7 +1173,7 @@ main {
.progress {
position: relative;
top: -39px;
top: -22px;
margin: auto;
margin-bottom: -12px;
display: block;
@ -1186,7 +1197,61 @@ main {
background: $blue;
}
}
.pre-image-uploader {
@include box-sizing(border-box);
@include baseline;
position: relative;
overflow:hidden;
height: auto;
color: $brown;
input {
position: absolute;
left: 9999px;
opacity: 0;
}
.image-edit {
line-height: 12px;
padding: 10px;
display: block;
position: absolute;
top: 0;
left: 0;
opacity: 0;
text-decoration: none;
}
}
//.progress {
// position: relative;
// top: -39px;
// margin: auto;
// margin-bottom: -12px;
// display: block;
// overflow: hidden;
// @include linear-gradient(to bottom, #f5f5f5, #f9f9f9);
// border-radius: 12px;
// box-shadow: (rgba(0,0,0,0.1) 0 1px 2px inset);
//}
//
//.fileupload-loading {
// display: block;
// top: 50%;
// width: 35px;
// height: 28px;
// margin: -28px auto 0;
// background-size: contain;
//}
//
//.bar {
// height: 12px;
// background: $blue;
//}
/* ==========================================================================
Misc

View File

@ -9,6 +9,7 @@
return '<section class="js-drop-zone image-uploader">' +
'<span class="media"><span class="hidden">Image Upload</span></span>' +
'<div class="description">Add image of <strong>' + alt + '</strong></div>' +
'<img class="js-upload-target" style="display: none" alt="alt" src="" />' +
'<input data-url="upload" class="js-fileupload fileupload" type="file" name="uploadimage">' +
'<a class="image-url" title="Add image from URL"><span class="hidden">URL</span></a>' +
'<a class="image-webcam" title="Add image from webcam">' +

View File

@ -15,13 +15,19 @@
<div class="form-group">
<label><strong>Blog Logo</strong></label>
<img src="/public/img/logo.png" alt="logo" height="38" width="381"/>
<section class="js-drop-zone">
<img id="logo" class="js-upload-target" src="{{logo}}"{{#unless logo}} style="display: none"{{/unless}} alt="logo"/>
<input data-url="upload" class="js-fileupload" type="file" name="uploadimage">
</section>
<p>Display a logo on your site in place of blog title</p>
</div>
<div class="form-group">
<label><strong>Blog Icon</strong></label>
<img src="/public/img/test-icon.png" alt="logo" height="38" width="38"/>
<section class="js-drop-zone">
<img id="icon" class="js-upload-target" src="{{icon}}"{{#unless icon}} style="display: none"{{/unless}} style="display: none" alt="icon"/>
<input data-url="upload" class="js-fileupload" type="file" name="uploadimage">
</section>
<p>The icon for your blog, used in your browser tab and elsewhere</p>
</div>

View File

@ -321,6 +321,7 @@
var view = this,
preview = document.getElementsByClassName('rendered-markdown')[0];
preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
view.$('.js-drop-zone').upload({editor: true});
Countable.once(preview, function (counter) {
view.$('.entry-word-count').text(counter.words + ' words');
view.$('.entry-character-count').text(counter.characters + ' characters');

View File

@ -128,7 +128,10 @@
saveSettings: function () {
this.model.save({
title: this.$('#blog-title').val(),
email: this.$('#email-address').val()
email: this.$('#email-address').val(),
logo: this.$('#logo').attr("src"),
icon: this.$('#icon').attr("src")
}, {
success: this.saveSuccess,
error: this.saveError
@ -141,6 +144,12 @@
var settings = this.model.toJSON();
this.$('#blog-title').val(settings.title);
this.$('#email-address').val(settings.email);
},
afterRender: function () {
this.$el.attr('id', this.id);
this.$el.addClass('active');
this.$('.js-drop-zone').upload();
}
});

View File

@ -2,11 +2,12 @@ var Ghost = require('../../ghost'),
dataExport = require('../data/export'),
dataImport = require('../data/import'),
_ = require('underscore'),
fs = require('fs'),
fs = require('fs-extra'),
path = require('path'),
when = require('when'),
nodefn = require('when/node/function'),
api = require('../api'),
moment = require('moment'),
ghost = new Ghost(),
dataProvider = ghost.dataProvider,
@ -54,6 +55,40 @@ function setSelected(list, name) {
}
adminControllers = {
'uploader': function (req, res) {
var currentDate = moment(),
month = currentDate.format("MMM"),
year = currentDate.format("YYYY"),
tmp_path = req.files.uploadimage.path,
dir = path.join('content/images', year, month),
target_path = path.join(dir, req.files.uploadimage.name),
ext = path.extname(req.files.uploadimage.name),
src = path.join('/', target_path);
function renameFile() {
// adds directories recursively
fs.mkdirs(dir, function (err) {
if (err) {
console.error(err);
} else {
fs.copy(tmp_path, target_path, function (err) {
if (err) {
console.error(err);
} else {
res.send(src);
}
});
}
});
}
if (ext === ".jpg" || ext === ".png" || ext === ".gif") {
renameFile();
} else {
res.send("Invalid filetype");
}
},
'login': function (req, res) {
res.render('login', {
bodyClass: 'ghost-login',

View File

@ -39,12 +39,18 @@
<script src="/shared/vendor/jquery/jquery.js"></script>
<script src="/shared/vendor/jquery/jquery-ui-1.10.3.custom.min.js"></script>
<script src="/public/lib/jquery-utils.js"></script>
<script src="/shared/vendor/underscore.js"></script>
<script src="/shared/vendor/backbone/backbone.js"></script>
<script src="/shared/vendor/handlebars/handlebars-runtime.js"></script>
<script src="/shared/vendor/moment.js"></script>
<script src="/public/vendor/icheck/jquery.icheck.min.js"></script>
<script src="/shared/vendor/jquery/jquery.ui.widget.js"></script>
<script src="/shared/vendor/jquery/jquery.iframe-transport.js"></script>
<script src="/shared/vendor/jquery/jquery.fileupload.js"></script>
<script src="/public/vendor/codemirror/codemirror.js"></script>
<script src="/public/vendor/codemirror/mode/markdown/markdown.js"></script>
<script src="/public/vendor/showdown/showdown.js"></script>
@ -54,8 +60,10 @@
<script src="/public/vendor/to-title-case.js"></script>
<script src="/public/vendor/packery.pkgd.min.js"></script>
<script src="/public/lib/jquery-utils.js"></script>
<script src="/public/init.js"></script>
<script src="/public/assets/lib/uploader.js"></script>
<script src="/public/tpl/hbs-tpl.js"></script>
<script src="/public/toggle.js"></script>
<script src="/public/markdown-actions.js"></script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
/*
* jQuery Iframe Transport Plugin 1.6.1
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
/*jslint unparam: true, nomen: true */
/*global define, window, document */
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define(['jquery'], factory);
} else {
// Browser globals:
factory(window.jQuery);
}
}(function ($) {
'use strict';
// Helper variable to create unique names for the transport iframes:
var counter = 0;
// The iframe transport accepts three additional options:
// options.fileInput: a jQuery collection of file input fields
// options.paramName: the parameter name for the file form data,
// overrides the name property of the file input field(s),
// can be a string or an array of strings.
// options.formData: an array of objects with name and value properties,
// equivalent to the return data of .serializeArray(), e.g.:
// [{name: 'a', value: 1}, {name: 'b', value: 2}]
$.ajaxTransport('iframe', function (options) {
if (options.async) {
var form,
iframe,
addParamChar;
return {
send: function (_, completeCallback) {
form = $('<form style="display:none;"></form>');
form.attr('accept-charset', options.formAcceptCharset);
addParamChar = /\?/.test(options.url) ? '&' : '?';
// XDomainRequest only supports GET and POST:
if (options.type === 'DELETE') {
options.url = options.url + addParamChar + '_method=DELETE';
options.type = 'POST';
} else if (options.type === 'PUT') {
options.url = options.url + addParamChar + '_method=PUT';
options.type = 'POST';
} else if (options.type === 'PATCH') {
options.url = options.url + addParamChar + '_method=PATCH';
options.type = 'POST';
}
// javascript:false as initial iframe src
// prevents warning popups on HTTPS in IE6.
// IE versions below IE8 cannot set the name property of
// elements that have already been added to the DOM,
// so we set the name along with the iframe HTML markup:
iframe = $(
'<iframe src="javascript:false;" name="iframe-transport-' +
(counter += 1) + '"></iframe>'
).bind('load', function () {
var fileInputClones,
paramNames = $.isArray(options.paramName) ?
options.paramName : [options.paramName];
iframe
.unbind('load')
.bind('load', function () {
var response;
// Wrap in a try/catch block to catch exceptions thrown
// when trying to access cross-domain iframe contents:
try {
response = iframe.contents();
// Google Chrome and Firefox do not throw an
// exception when calling iframe.contents() on
// cross-domain requests, so we unify the response:
if (!response.length || !response[0].firstChild) {
throw new Error();
}
} catch (e) {
response = undefined;
}
// The complete callback returns the
// iframe content document as response object:
completeCallback(
200,
'success',
{'iframe': response}
);
// Fix for IE endless progress bar activity bug
// (happens on form submits to iframe targets):
$('<iframe src="javascript:false;"></iframe>')
.appendTo(form);
form.remove();
});
form
.prop('target', iframe.prop('name'))
.prop('action', options.url)
.prop('method', options.type);
if (options.formData) {
$.each(options.formData, function (index, field) {
$('<input type="hidden"/>')
.prop('name', field.name)
.val(field.value)
.appendTo(form);
});
}
if (options.fileInput && options.fileInput.length &&
options.type === 'POST') {
fileInputClones = options.fileInput.clone();
// Insert a clone for each file input field:
options.fileInput.after(function (index) {
return fileInputClones[index];
});
if (options.paramName) {
options.fileInput.each(function (index) {
$(this).prop(
'name',
paramNames[index] || options.paramName
);
});
}
// Appending the file input fields to the hidden form
// removes them from their original location:
form
.append(options.fileInput)
.prop('enctype', 'multipart/form-data')
// enctype must be set as encoding for IE:
.prop('encoding', 'multipart/form-data');
}
form.submit();
// Insert the file input fields at their original location
// by replacing the clones with the originals:
if (fileInputClones && fileInputClones.length) {
options.fileInput.each(function (index, input) {
var clone = $(fileInputClones[index]);
$(input).prop('name', clone.prop('name'));
clone.replaceWith(input);
});
}
});
form.append(iframe).appendTo(document.body);
},
abort: function () {
if (iframe) {
// javascript:false as iframe src aborts the request
// and prevents warning popups on HTTPS in IE6.
// concat is used to avoid the "Script URL" JSLint error:
iframe
.unbind('load')
.prop('src', 'javascript'.concat(':false;'));
}
if (form) {
form.remove();
}
}
};
}
});
// The iframe transport returns the iframe content document as response.
// The following adds converters from iframe to text, json, html, and script:
$.ajaxSetup({
converters: {
'iframe text': function (iframe) {
return iframe && $(iframe[0].body).text();
},
'iframe json': function (iframe) {
return iframe && $.parseJSON($(iframe[0].body).text());
},
'iframe html': function (iframe) {
return iframe && $(iframe[0].body).html();
},
'iframe script': function (iframe) {
return iframe && $.globalEval($(iframe[0].body).text());
}
}
});
}));

View File

@ -0,0 +1,530 @@
/*
* jQuery UI Widget 1.10.1+amd
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/jQuery.widget/
*/
(function (factory) {
if (typeof define === "function" && define.amd) {
// Register as an anonymous AMD module:
define(["jquery"], factory);
} else {
// Browser globals:
factory(jQuery);
}
}(function( $, undefined ) {
var uuid = 0,
slice = Array.prototype.slice,
_cleanData = $.cleanData;
$.cleanData = function( elems ) {
for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
try {
$( elem ).triggerHandler( "remove" );
// http://bugs.jquery.com/ticket/8235
} catch( e ) {}
}
_cleanData( elems );
};
$.widget = function( name, base, prototype ) {
var fullName, existingConstructor, constructor, basePrototype,
// proxiedPrototype allows the provided prototype to remain unmodified
// so that it can be used as a mixin for multiple widgets (#8876)
proxiedPrototype = {},
namespace = name.split( "." )[ 0 ];
name = name.split( "." )[ 1 ];
fullName = namespace + "-" + name;
if ( !prototype ) {
prototype = base;
base = $.Widget;
}
// create selector for plugin
$.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) {
return !!$.data( elem, fullName );
};
$[ namespace ] = $[ namespace ] || {};
existingConstructor = $[ namespace ][ name ];
constructor = $[ namespace ][ name ] = function( options, element ) {
// allow instantiation without "new" keyword
if ( !this._createWidget ) {
return new constructor( options, element );
}
// allow instantiation without initializing for simple inheritance
// must use "new" keyword (the code above always passes args)
if ( arguments.length ) {
this._createWidget( options, element );
}
};
// extend with the existing constructor to carry over any static properties
$.extend( constructor, existingConstructor, {
version: prototype.version,
// copy the object used to create the prototype in case we need to
// redefine the widget later
_proto: $.extend( {}, prototype ),
// track widgets that inherit from this widget in case this widget is
// redefined after a widget inherits from it
_childConstructors: []
});
basePrototype = new base();
// we need to make the options hash a property directly on the new instance
// otherwise we'll modify the options hash on the prototype that we're
// inheriting from
basePrototype.options = $.widget.extend( {}, basePrototype.options );
$.each( prototype, function( prop, value ) {
if ( !$.isFunction( value ) ) {
proxiedPrototype[ prop ] = value;
return;
}
proxiedPrototype[ prop ] = (function() {
var _super = function() {
return base.prototype[ prop ].apply( this, arguments );
},
_superApply = function( args ) {
return base.prototype[ prop ].apply( this, args );
};
return function() {
var __super = this._super,
__superApply = this._superApply,
returnValue;
this._super = _super;
this._superApply = _superApply;
returnValue = value.apply( this, arguments );
this._super = __super;
this._superApply = __superApply;
return returnValue;
};
})();
});
constructor.prototype = $.widget.extend( basePrototype, {
// TODO: remove support for widgetEventPrefix
// always use the name + a colon as the prefix, e.g., draggable:start
// don't prefix for widgets that aren't DOM-based
widgetEventPrefix: existingConstructor ? basePrototype.widgetEventPrefix : name
}, proxiedPrototype, {
constructor: constructor,
namespace: namespace,
widgetName: name,
widgetFullName: fullName
});
// If this widget is being redefined then we need to find all widgets that
// are inheriting from it and redefine all of them so that they inherit from
// the new version of this widget. We're essentially trying to replace one
// level in the prototype chain.
if ( existingConstructor ) {
$.each( existingConstructor._childConstructors, function( i, child ) {
var childPrototype = child.prototype;
// redefine the child widget using the same prototype that was
// originally used, but inherit from the new version of the base
$.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto );
});
// remove the list of existing child constructors from the old constructor
// so the old child constructors can be garbage collected
delete existingConstructor._childConstructors;
} else {
base._childConstructors.push( constructor );
}
$.widget.bridge( name, constructor );
};
$.widget.extend = function( target ) {
var input = slice.call( arguments, 1 ),
inputIndex = 0,
inputLength = input.length,
key,
value;
for ( ; inputIndex < inputLength; inputIndex++ ) {
for ( key in input[ inputIndex ] ) {
value = input[ inputIndex ][ key ];
if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) {
// Clone objects
if ( $.isPlainObject( value ) ) {
target[ key ] = $.isPlainObject( target[ key ] ) ?
$.widget.extend( {}, target[ key ], value ) :
// Don't extend strings, arrays, etc. with objects
$.widget.extend( {}, value );
// Copy everything else by reference
} else {
target[ key ] = value;
}
}
}
}
return target;
};
$.widget.bridge = function( name, object ) {
var fullName = object.prototype.widgetFullName || name;
$.fn[ name ] = function( options ) {
var isMethodCall = typeof options === "string",
args = slice.call( arguments, 1 ),
returnValue = this;
// allow multiple hashes to be passed on init
options = !isMethodCall && args.length ?
$.widget.extend.apply( null, [ options ].concat(args) ) :
options;
if ( isMethodCall ) {
this.each(function() {
var methodValue,
instance = $.data( this, fullName );
if ( !instance ) {
return $.error( "cannot call methods on " + name + " prior to initialization; " +
"attempted to call method '" + options + "'" );
}
if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) {
return $.error( "no such method '" + options + "' for " + name + " widget instance" );
}
methodValue = instance[ options ].apply( instance, args );
if ( methodValue !== instance && methodValue !== undefined ) {
returnValue = methodValue && methodValue.jquery ?
returnValue.pushStack( methodValue.get() ) :
methodValue;
return false;
}
});
} else {
this.each(function() {
var instance = $.data( this, fullName );
if ( instance ) {
instance.option( options || {} )._init();
} else {
$.data( this, fullName, new object( options, this ) );
}
});
}
return returnValue;
};
};
$.Widget = function( /* options, element */ ) {};
$.Widget._childConstructors = [];
$.Widget.prototype = {
widgetName: "widget",
widgetEventPrefix: "",
defaultElement: "<div>",
options: {
disabled: false,
// callbacks
create: null
},
_createWidget: function( options, element ) {
element = $( element || this.defaultElement || this )[ 0 ];
this.element = $( element );
this.uuid = uuid++;
this.eventNamespace = "." + this.widgetName + this.uuid;
this.options = $.widget.extend( {},
this.options,
this._getCreateOptions(),
options );
this.bindings = $();
this.hoverable = $();
this.focusable = $();
if ( element !== this ) {
$.data( element, this.widgetFullName, this );
this._on( true, this.element, {
remove: function( event ) {
if ( event.target === element ) {
this.destroy();
}
}
});
this.document = $( element.style ?
// element within the document
element.ownerDocument :
// element is window or document
element.document || element );
this.window = $( this.document[0].defaultView || this.document[0].parentWindow );
}
this._create();
this._trigger( "create", null, this._getCreateEventData() );
this._init();
},
_getCreateOptions: $.noop,
_getCreateEventData: $.noop,
_create: $.noop,
_init: $.noop,
destroy: function() {
this._destroy();
// we can probably remove the unbind calls in 2.0
// all event bindings should go through this._on()
this.element
.unbind( this.eventNamespace )
// 1.9 BC for #7810
// TODO remove dual storage
.removeData( this.widgetName )
.removeData( this.widgetFullName )
// support: jquery <1.6.3
// http://bugs.jquery.com/ticket/9413
.removeData( $.camelCase( this.widgetFullName ) );
this.widget()
.unbind( this.eventNamespace )
.removeAttr( "aria-disabled" )
.removeClass(
this.widgetFullName + "-disabled " +
"ui-state-disabled" );
// clean up events and states
this.bindings.unbind( this.eventNamespace );
this.hoverable.removeClass( "ui-state-hover" );
this.focusable.removeClass( "ui-state-focus" );
},
_destroy: $.noop,
widget: function() {
return this.element;
},
option: function( key, value ) {
var options = key,
parts,
curOption,
i;
if ( arguments.length === 0 ) {
// don't return a reference to the internal hash
return $.widget.extend( {}, this.options );
}
if ( typeof key === "string" ) {
// handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
options = {};
parts = key.split( "." );
key = parts.shift();
if ( parts.length ) {
curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
for ( i = 0; i < parts.length - 1; i++ ) {
curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
curOption = curOption[ parts[ i ] ];
}
key = parts.pop();
if ( value === undefined ) {
return curOption[ key ] === undefined ? null : curOption[ key ];
}
curOption[ key ] = value;
} else {
if ( value === undefined ) {
return this.options[ key ] === undefined ? null : this.options[ key ];
}
options[ key ] = value;
}
}
this._setOptions( options );
return this;
},
_setOptions: function( options ) {
var key;
for ( key in options ) {
this._setOption( key, options[ key ] );
}
return this;
},
_setOption: function( key, value ) {
this.options[ key ] = value;
if ( key === "disabled" ) {
this.widget()
.toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value )
.attr( "aria-disabled", value );
this.hoverable.removeClass( "ui-state-hover" );
this.focusable.removeClass( "ui-state-focus" );
}
return this;
},
enable: function() {
return this._setOption( "disabled", false );
},
disable: function() {
return this._setOption( "disabled", true );
},
_on: function( suppressDisabledCheck, element, handlers ) {
var delegateElement,
instance = this;
// no suppressDisabledCheck flag, shuffle arguments
if ( typeof suppressDisabledCheck !== "boolean" ) {
handlers = element;
element = suppressDisabledCheck;
suppressDisabledCheck = false;
}
// no element argument, shuffle and use this.element
if ( !handlers ) {
handlers = element;
element = this.element;
delegateElement = this.widget();
} else {
// accept selectors, DOM elements
element = delegateElement = $( element );
this.bindings = this.bindings.add( element );
}
$.each( handlers, function( event, handler ) {
function handlerProxy() {
// allow widgets to customize the disabled handling
// - disabled as an array instead of boolean
// - disabled class as method for disabling individual parts
if ( !suppressDisabledCheck &&
( instance.options.disabled === true ||
$( this ).hasClass( "ui-state-disabled" ) ) ) {
return;
}
return ( typeof handler === "string" ? instance[ handler ] : handler )
.apply( instance, arguments );
}
// copy the guid so direct unbinding works
if ( typeof handler !== "string" ) {
handlerProxy.guid = handler.guid =
handler.guid || handlerProxy.guid || $.guid++;
}
var match = event.match( /^(\w+)\s*(.*)$/ ),
eventName = match[1] + instance.eventNamespace,
selector = match[2];
if ( selector ) {
delegateElement.delegate( selector, eventName, handlerProxy );
} else {
element.bind( eventName, handlerProxy );
}
});
},
_off: function( element, eventName ) {
eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace;
element.unbind( eventName ).undelegate( eventName );
},
_delay: function( handler, delay ) {
function handlerProxy() {
return ( typeof handler === "string" ? instance[ handler ] : handler )
.apply( instance, arguments );
}
var instance = this;
return setTimeout( handlerProxy, delay || 0 );
},
_hoverable: function( element ) {
this.hoverable = this.hoverable.add( element );
this._on( element, {
mouseenter: function( event ) {
$( event.currentTarget ).addClass( "ui-state-hover" );
},
mouseleave: function( event ) {
$( event.currentTarget ).removeClass( "ui-state-hover" );
}
});
},
_focusable: function( element ) {
this.focusable = this.focusable.add( element );
this._on( element, {
focusin: function( event ) {
$( event.currentTarget ).addClass( "ui-state-focus" );
},
focusout: function( event ) {
$( event.currentTarget ).removeClass( "ui-state-focus" );
}
});
},
_trigger: function( type, event, data ) {
var prop, orig,
callback = this.options[ type ];
data = data || {};
event = $.Event( event );
event.type = ( type === this.widgetEventPrefix ?
type :
this.widgetEventPrefix + type ).toLowerCase();
// the original event may come from any element
// so we need to reset the target on the new event
event.target = this.element[ 0 ];
// copy original event properties over to the new event
orig = event.originalEvent;
if ( orig ) {
for ( prop in orig ) {
if ( !( prop in event ) ) {
event[ prop ] = orig[ prop ];
}
}
}
this.element.trigger( event, data );
return !( $.isFunction( callback ) &&
callback.apply( this.element[0], [ event ].concat( data ) ) === false ||
event.isDefaultPrevented() );
}
};
$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
$.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
if ( typeof options === "string" ) {
options = { effect: options };
}
var hasOptions,
effectName = !options ?
method :
options === true || typeof options === "number" ?
defaultEffect :
options.effect || defaultEffect;
options = options || {};
if ( typeof options === "number" ) {
options = { duration: options };
}
hasOptions = !$.isEmptyObject( options );
options.complete = callback;
if ( options.delay ) {
element.delay( options.delay );
}
if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
element[ method ]( options );
} else if ( effectName !== method && element[ effectName ] ) {
element[ effectName ]( options.duration, options.easing, callback );
} else {
element.queue(function( next ) {
$( this )[ method ]();
if ( callback ) {
callback.call( element[ 0 ] );
}
next();
});
}
};
});
}));

View File

@ -126,6 +126,7 @@ ghost.app().configure(function () {
ghost.app().use(express.favicon(__dirname + '/content/images/favicon.ico'));
ghost.app().use(I18n.load(ghost));
ghost.app().use(express.bodyParser({}));
ghost.app().use(express.bodyParser({uploadDir: __dirname + '/content/images'}));
ghost.app().use(express.cookieParser('try-ghost'));
ghost.app().use(express.cookieSession({ cookie: { maxAge: 60000000 }}));
ghost.app().use(ghost.initTheme(ghost.app()));
@ -195,6 +196,7 @@ when.all([ghost.init(), filters.loadCoreFilters(ghost), helpers.loadCoreHelpers(
ghost.app().del('/api/v0.1/notifications/:id', authAPI, disableCachedResult, api.requestHandler(api.notifications.destroy));
ghost.app().post('/api/v0.1/notifications/', authAPI, disableCachedResult, api.requestHandler(api.notifications.add));
ghost.app().post('/ghost/upload', admin.uploader);
/**
* Frontend routes..

View File

@ -25,7 +25,8 @@
"bcrypt-nodejs": "0.0.3",
"node-uuid": "1.4.0",
"colors": "0.6.1",
"semver": "2.1.0"
"semver": "2.1.0",
"fs-extra": "0.6.3"
},
"devDependencies": {
"grunt": "~0.4.1",