├── .gitignore ├── README.md ├── app.py ├── data ├── .gitignore └── thumbnail │ └── .gitignore ├── lib ├── __init__.py └── upload_file.py ├── requirements.txt ├── server_config ├── init.d_script.sh ├── nginx.ini └── uwsgi.ini ├── static ├── css │ ├── demo-ie8.css │ ├── demo.css │ ├── jquery.fileupload-noscript.css │ ├── jquery.fileupload-ui-noscript.css │ ├── jquery.fileupload-ui.css │ ├── jquery.fileupload.css │ └── style.css ├── img │ ├── loading.gif │ └── progressbar.gif └── js │ ├── app.js │ ├── cors │ ├── jquery.postmessage-transport.js │ └── jquery.xdr-transport.js │ ├── jquery.fileupload-angular.js │ ├── jquery.fileupload-audio.js │ ├── jquery.fileupload-image.js │ ├── jquery.fileupload-jquery-ui.js │ ├── jquery.fileupload-process.js │ ├── jquery.fileupload-ui.js │ ├── jquery.fileupload-validate.js │ ├── jquery.fileupload-video.js │ ├── jquery.fileupload.js │ ├── jquery.iframe-transport.js │ ├── main.js │ └── vendor │ └── jquery.ui.widget.js ├── system_package.txt └── templates ├── 404.html ├── 500.html ├── base.html └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | # gedit 2 | *~ 3 | 4 | # eclipse 5 | .settings 6 | 7 | # pycharm 8 | .idea 9 | 10 | *.py[cod] 11 | 12 | # Installer logs 13 | pip-log.txt 14 | 15 | # Unit test / coverage reports 16 | .coverage 17 | .tox 18 | nosetests.xml 19 | 20 | # tmp files 21 | .~* 22 | 23 | logfile.log 24 | 25 | flask 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | flask-file-uploader 2 | =================== 3 | 4 | ## Description 5 | File Upload Script which built on Python Flask and [jQuery-File-Upload](https://github.com/blueimp/jQuery-File-Upload/) with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery. 6 | 7 | 8 | ## Setup 9 | - Install system package. See the `system_package.txt` file. (*Unix) 10 | 11 | - Create virtual enviroment (use `virtualenv`) and activate it. 12 | 13 | - Then install python packages: 14 | ``` 15 | $ pip install -r requirements.txt 16 | ``` 17 | 18 | - Run it: 19 | 20 | ``` 21 | $ python app.py 22 | ``` 23 | 24 | - Go to http://127.0.0.1:9191 25 | 26 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!flask/bin/python 2 | 3 | # Author: Ngo Duy Khanh 4 | # Email: ngokhanhit@gmail.com 5 | # Git repository: https://github.com/ngoduykhanh/flask-file-uploader 6 | # This work based on jQuery-File-Upload which can be found at https://github.com/blueimp/jQuery-File-Upload/ 7 | 8 | import os 9 | import PIL 10 | from PIL import Image 11 | import simplejson 12 | import traceback 13 | 14 | from flask import Flask, request, render_template, redirect, url_for, send_from_directory 15 | from flask_bootstrap import Bootstrap 16 | from werkzeug import secure_filename 17 | 18 | from lib.upload_file import uploadfile 19 | 20 | 21 | app = Flask(__name__) 22 | app.config['SECRET_KEY'] = 'hard to guess string' 23 | app.config['UPLOAD_FOLDER'] = 'data/' 24 | app.config['THUMBNAIL_FOLDER'] = 'data/thumbnail/' 25 | app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 26 | 27 | ALLOWED_EXTENSIONS = set(['txt', 'gif', 'png', 'jpg', 'jpeg', 'bmp', 'rar', 'zip', '7zip', 'doc', 'docx']) 28 | IGNORED_FILES = set(['.gitignore']) 29 | 30 | bootstrap = Bootstrap(app) 31 | 32 | 33 | def allowed_file(filename): 34 | return '.' in filename and \ 35 | filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS 36 | 37 | 38 | def gen_file_name(filename): 39 | """ 40 | If file was exist already, rename it and return a new name 41 | """ 42 | 43 | i = 1 44 | while os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], filename)): 45 | name, extension = os.path.splitext(filename) 46 | filename = '%s_%s%s' % (name, str(i), extension) 47 | i += 1 48 | 49 | return filename 50 | 51 | 52 | def create_thumbnail(image): 53 | try: 54 | base_width = 80 55 | img = Image.open(os.path.join(app.config['UPLOAD_FOLDER'], image)) 56 | w_percent = (base_width / float(img.size[0])) 57 | h_size = int((float(img.size[1]) * float(w_percent))) 58 | img = img.resize((base_width, h_size), PIL.Image.ANTIALIAS) 59 | img.save(os.path.join(app.config['THUMBNAIL_FOLDER'], image)) 60 | 61 | return True 62 | 63 | except: 64 | print traceback.format_exc() 65 | return False 66 | 67 | 68 | @app.route("/upload", methods=['GET', 'POST']) 69 | def upload(): 70 | if request.method == 'POST': 71 | files = request.files['file'] 72 | 73 | if files: 74 | filename = secure_filename(files.filename) 75 | filename = gen_file_name(filename) 76 | mime_type = files.content_type 77 | 78 | if not allowed_file(files.filename): 79 | result = uploadfile(name=filename, type=mime_type, size=0, not_allowed_msg="File type not allowed") 80 | 81 | else: 82 | # save file to disk 83 | uploaded_file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) 84 | files.save(uploaded_file_path) 85 | 86 | # create thumbnail after saving 87 | if mime_type.startswith('image'): 88 | create_thumbnail(filename) 89 | 90 | # get file size after saving 91 | size = os.path.getsize(uploaded_file_path) 92 | 93 | # return json for js call back 94 | result = uploadfile(name=filename, type=mime_type, size=size) 95 | 96 | return simplejson.dumps({"files": [result.get_file()]}) 97 | 98 | if request.method == 'GET': 99 | # get all file in ./data directory 100 | files = [f for f in os.listdir(app.config['UPLOAD_FOLDER']) if os.path.isfile(os.path.join(app.config['UPLOAD_FOLDER'],f)) and f not in IGNORED_FILES ] 101 | 102 | file_display = [] 103 | 104 | for f in files: 105 | size = os.path.getsize(os.path.join(app.config['UPLOAD_FOLDER'], f)) 106 | file_saved = uploadfile(name=f, size=size) 107 | file_display.append(file_saved.get_file()) 108 | 109 | return simplejson.dumps({"files": file_display}) 110 | 111 | return redirect(url_for('index')) 112 | 113 | 114 | @app.route("/delete/", methods=['DELETE']) 115 | def delete(filename): 116 | file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) 117 | file_thumb_path = os.path.join(app.config['THUMBNAIL_FOLDER'], filename) 118 | 119 | if os.path.exists(file_path): 120 | try: 121 | os.remove(file_path) 122 | 123 | if os.path.exists(file_thumb_path): 124 | os.remove(file_thumb_path) 125 | 126 | return simplejson.dumps({filename: 'True'}) 127 | except: 128 | return simplejson.dumps({filename: 'False'}) 129 | 130 | 131 | # serve static files 132 | @app.route("/thumbnail/", methods=['GET']) 133 | def get_thumbnail(filename): 134 | return send_from_directory(app.config['THUMBNAIL_FOLDER'], filename=filename) 135 | 136 | 137 | @app.route("/data/", methods=['GET']) 138 | def get_file(filename): 139 | return send_from_directory(os.path.join(app.config['UPLOAD_FOLDER']), filename=filename) 140 | 141 | 142 | @app.route('/', methods=['GET', 'POST']) 143 | def index(): 144 | return render_template('index.html') 145 | 146 | 147 | if __name__ == '__main__': 148 | app.run(debug=True, port=9191) 149 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | !thumbnail 6 | -------------------------------------------------------------------------------- /data/thumbnail/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greyli/flask-file-uploader/27d4435573be9a4cff1478b72b76e56c64464cc5/lib/__init__.py -------------------------------------------------------------------------------- /lib/upload_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class uploadfile(): 4 | def __init__(self, name, type=None, size=None, not_allowed_msg=''): 5 | self.name = name 6 | self.type = type 7 | self.size = size 8 | self.not_allowed_msg = not_allowed_msg 9 | self.url = "data/%s" % name 10 | self.thumbnail_url = "thumbnail/%s" % name 11 | self.delete_url = "delete/%s" % name 12 | self.delete_type = "DELETE" 13 | 14 | 15 | def is_image(self): 16 | fileName, fileExtension = os.path.splitext(self.name.lower()) 17 | 18 | if fileExtension in ['.jpg', '.png', '.jpeg', '.bmp']: 19 | return True 20 | 21 | return False 22 | 23 | 24 | def get_file(self): 25 | if self.type != None: 26 | # POST an image 27 | if self.type.startswith('image'): 28 | return {"name": self.name, 29 | "type": self.type, 30 | "size": self.size, 31 | "url": self.url, 32 | "thumbnailUrl": self.thumbnail_url, 33 | "deleteUrl": self.delete_url, 34 | "deleteType": self.delete_type,} 35 | 36 | # POST an normal file 37 | elif self.not_allowed_msg == '': 38 | return {"name": self.name, 39 | "type": self.type, 40 | "size": self.size, 41 | "url": self.url, 42 | "deleteUrl": self.delete_url, 43 | "deleteType": self.delete_type,} 44 | 45 | # File type is not allowed 46 | else: 47 | return {"error": self.not_allowed_msg, 48 | "name": self.name, 49 | "type": self.type, 50 | "size": self.size,} 51 | 52 | # GET image from disk 53 | elif self.is_image(): 54 | return {"name": self.name, 55 | "size": self.size, 56 | "url": self.url, 57 | "thumbnailUrl": self.thumbnail_url, 58 | "deleteUrl": self.delete_url, 59 | "deleteType": self.delete_type,} 60 | 61 | # GET normal file from disk 62 | else: 63 | return {"name": self.name, 64 | "size": self.size, 65 | "url": self.url, 66 | "deleteUrl": self.delete_url, 67 | "deleteType": self.delete_type,} 68 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10 2 | Flask-Bootstrap==3.2.0.2 3 | simplejson==3.6.0 4 | Pillow==2.5.1 5 | -------------------------------------------------------------------------------- /server_config/init.d_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # uwsgi - Use uwsgi to run python and wsgi web apps. 3 | # 4 | # chkconfig: - 85 15 5 | # description: Use uwsgi to run python and wsgi web apps. 6 | # processname: uwsgi 7 | 8 | CONF=/opt/yourwebsite.com/server_config/uwsgi.ini 9 | PATH=/opt/uwsgi:/sbin:/bin:/usr/sbin:/usr/bin 10 | DAEMON=/usr/bin/uwsgi 11 | 12 | OWNER=root 13 | NAME=uwsgi_yourwebsite.com 14 | DESC=uwsgi_yourwebsite.com 15 | 16 | pidfile=/var/run/${NAME}.pid 17 | 18 | test -x $DAEMON || exit 0 19 | 20 | # Include uwsgi defaults if available 21 | if [ -f /etc/default/uwsgi ] ; then 22 | . /etc/default/uwsgi 23 | fi 24 | 25 | set -e 26 | 27 | get_pid() { 28 | if [ -f $pidfile ]; then 29 | echo `cat $pidfile` 30 | fi 31 | } 32 | 33 | DAEMON_OPTS="--pidfile /var/run/$NAME.pid $CONF" 34 | 35 | case "$1" in 36 | start) 37 | echo -n "Starting $DESC: " 38 | PID=$(get_pid) 39 | if [ -z "$PID" ]; then 40 | [ -f $pidfile ] && rm -f $pidfile 41 | 42 | touch $pidfile 43 | chown $OWNER $pidfile 44 | su - $OWNER -pc "$DAEMON $DAEMON_OPTS > /dev/null 2>&1 &" 45 | echo "$NAME." 46 | fi 47 | 48 | ;; 49 | stop) 50 | echo -n "Stopping $DESC: " 51 | PID=$(get_pid) 52 | [ ! -z "$PID" ] && kill -s 3 $PID &> /dev/null 53 | if [ $? -gt 0 ]; then 54 | echo "was not running" 55 | else 56 | echo "$NAME." 57 | rm -f $pidfile &> /dev/null 58 | fi 59 | ;; 60 | 61 | reload) 62 | echo "Reloading $NAME" 63 | PID=$(get_pid) 64 | [ ! -z "$PID" ] && kill -s 1 $PID &> /dev/null 65 | if [ $? -gt 0 ]; then 66 | echo "was not running" 67 | exit 1 68 | else 69 | echo "$NAME." 70 | rm -f /var/run/$NAME.pid &> /dev/null 71 | fi 72 | ;; 73 | force-reload) 74 | echo "Reloading $NAME" 75 | PID=$(get_pid) 76 | [ ! -z "$PID" ] && kill -s 15 $PID &> /dev/null 77 | if [ $? -gt 0 ]; then 78 | echo "was not running" 79 | exit 1 80 | else 81 | echo "$NAME." 82 | rm -f /var/run/$NAME.pid &> /dev/null 83 | fi 84 | ;; 85 | restart) 86 | $0 stop 87 | sleep 2 88 | $0 start 89 | ;; 90 | status) 91 | killall -10 $DAEMON 92 | ;; 93 | *) 94 | N=/etc/init.d/$NAME 95 | echo "Usage: $N {start|stop|restart|reload|force-reload|status}" >&2 96 | exit 1 97 | ;; 98 | esac 99 | exit 0 -------------------------------------------------------------------------------- /server_config/nginx.ini: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name yourwebsite.com; 4 | 5 | access_log /var/log/nginx/yourwebsite.com.access.log; 6 | error_log /var/log/nginx/yourwebsite.com.error.log; 7 | 8 | location / { 9 | include uwsgi_params; 10 | uwsgi_pass 127.0.0.1:9191; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server_config/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | processes = 4 3 | threads = 2 4 | #http = 127.0.0.1:9191 5 | socket = 127.0.0.1:9191 6 | stats = 127.0.0.1:9090 7 | chdir = /opt/yourwebsite.com/ 8 | virtualenv = /opt/yourwebsite.com/flask/ 9 | module = app 10 | callable = app -------------------------------------------------------------------------------- /static/css/demo-ie8.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload Demo CSS Fixes for IE<9 1.0.0 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2013, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT 11 | */ 12 | 13 | .navigation { 14 | list-style: none; 15 | padding: 0; 16 | margin: 1em 0; 17 | } 18 | .navigation li { 19 | display: inline; 20 | margin-right: 10px; 21 | } 22 | -------------------------------------------------------------------------------- /static/css/demo.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload Demo CSS 1.1.0 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2013, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT 11 | */ 12 | 13 | body { 14 | max-width: 750px; 15 | margin: 0 auto; 16 | padding: 1em; 17 | font-family: "Lucida Grande", "Lucida Sans Unicode", Arial, sans-serif; 18 | font-size: 1em; 19 | line-height: 1.4em; 20 | background: #222; 21 | color: #fff; 22 | -webkit-text-size-adjust: 100%; 23 | -ms-text-size-adjust: 100%; 24 | } 25 | a { 26 | color: orange; 27 | text-decoration: none; 28 | } 29 | img { 30 | border: 0; 31 | vertical-align: middle; 32 | } 33 | h1 { 34 | line-height: 1em; 35 | } 36 | blockquote { 37 | padding: 0 0 0 15px; 38 | margin: 0 0 20px; 39 | border-left: 5px solid #eee; 40 | } 41 | table { 42 | width: 100%; 43 | margin: 10px 0; 44 | } 45 | 46 | .fileupload-progress { 47 | margin: 10px 0; 48 | } 49 | .fileupload-progress .progress-extended { 50 | margin-top: 5px; 51 | } 52 | .error { 53 | color: red; 54 | } 55 | 56 | @media (min-width: 481px) { 57 | .navigation { 58 | list-style: none; 59 | padding: 0; 60 | } 61 | .navigation li { 62 | display: inline-block; 63 | } 64 | .navigation li:not(:first-child):before { 65 | content: "| "; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /static/css/jquery.fileupload-noscript.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload Plugin NoScript CSS 1.2.0 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2013, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileinput-button input { 14 | position: static; 15 | opacity: 1; 16 | filter: none; 17 | font-size: inherit; 18 | direction: inherit; 19 | } 20 | .fileinput-button span { 21 | display: none; 22 | } 23 | -------------------------------------------------------------------------------- /static/css/jquery.fileupload-ui-noscript.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload UI Plugin NoScript CSS 8.8.5 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2012, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileinput-button i, 14 | .fileupload-buttonbar .delete, 15 | .fileupload-buttonbar .toggle { 16 | display: none; 17 | } 18 | -------------------------------------------------------------------------------- /static/css/jquery.fileupload-ui.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload UI Plugin CSS 9.0.0 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2010, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileupload-buttonbar .btn, 14 | .fileupload-buttonbar .toggle { 15 | margin-bottom: 5px; 16 | } 17 | .progress-animated .progress-bar, 18 | .progress-animated .bar { 19 | background: url("../img/progressbar.gif") !important; 20 | filter: none; 21 | } 22 | .fileupload-process { 23 | float: right; 24 | display: none; 25 | } 26 | .fileupload-processing .fileupload-process, 27 | .files .processing .preview { 28 | display: block; 29 | width: 32px; 30 | height: 32px; 31 | background: url("../img/loading.gif") center no-repeat; 32 | background-size: contain; 33 | } 34 | .files audio, 35 | .files video { 36 | max-width: 300px; 37 | } 38 | 39 | @media (max-width: 767px) { 40 | .fileupload-buttonbar .toggle, 41 | .files .toggle, 42 | .files .btn span { 43 | display: none; 44 | } 45 | .files .name { 46 | width: 80px; 47 | word-wrap: break-word; 48 | } 49 | .files audio, 50 | .files video { 51 | max-width: 80px; 52 | } 53 | .files img, 54 | .files canvas { 55 | max-width: 100%; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /static/css/jquery.fileupload.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * jQuery File Upload Plugin CSS 1.3.0 4 | * https://github.com/blueimp/jQuery-File-Upload 5 | * 6 | * Copyright 2013, Sebastian Tschan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * http://www.opensource.org/licenses/MIT 11 | */ 12 | 13 | .fileinput-button { 14 | position: relative; 15 | overflow: hidden; 16 | } 17 | .fileinput-button input { 18 | position: absolute; 19 | top: 0; 20 | right: 0; 21 | margin: 0; 22 | opacity: 0; 23 | -ms-filter: 'alpha(opacity=0)'; 24 | font-size: 200px; 25 | direction: ltr; 26 | cursor: pointer; 27 | } 28 | 29 | /* Fixes for IE < 8 */ 30 | @media screen\9 { 31 | .fileinput-button input { 32 | filter: alpha(opacity=0); 33 | font-size: 100%; 34 | height: 100%; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | body { 4 | padding-top: 60px; 5 | } 6 | */ -------------------------------------------------------------------------------- /static/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greyli/flask-file-uploader/27d4435573be9a4cff1478b72b76e56c64464cc5/static/img/loading.gif -------------------------------------------------------------------------------- /static/img/progressbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greyli/flask-file-uploader/27d4435573be9a4cff1478b72b76e56c64464cc5/static/img/progressbar.gif -------------------------------------------------------------------------------- /static/js/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Plugin Angular JS Example 1.2.1 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* jshint nomen:false */ 13 | /* global window, angular */ 14 | 15 | (function () { 16 | 'use strict'; 17 | 18 | var isOnGitHub = window.location.hostname === 'blueimp.github.io', 19 | url = isOnGitHub ? '//jquery-file-upload.appspot.com/' : 'server/php/'; 20 | 21 | angular.module('demo', [ 22 | 'blueimp.fileupload' 23 | ]) 24 | .config([ 25 | '$httpProvider', 'fileUploadProvider', 26 | function ($httpProvider, fileUploadProvider) { 27 | delete $httpProvider.defaults.headers.common['X-Requested-With']; 28 | fileUploadProvider.defaults.redirect = window.location.href.replace( 29 | /\/[^\/]*$/, 30 | '/cors/result.html?%s' 31 | ); 32 | if (isOnGitHub) { 33 | // Demo settings: 34 | angular.extend(fileUploadProvider.defaults, { 35 | // Enable image resizing, except for Android and Opera, 36 | // which actually support image resizing, but fail to 37 | // send Blob objects via XHR requests: 38 | disableImageResize: /Android(?!.*Chrome)|Opera/ 39 | .test(window.navigator.userAgent), 40 | maxFileSize: 5000000, 41 | acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i 42 | }); 43 | } 44 | } 45 | ]) 46 | 47 | .controller('DemoFileUploadController', [ 48 | '$scope', '$http', '$filter', '$window', 49 | function ($scope, $http) { 50 | $scope.options = { 51 | url: url 52 | }; 53 | if (!isOnGitHub) { 54 | $scope.loadingFiles = true; 55 | $http.get(url) 56 | .then( 57 | function (response) { 58 | $scope.loadingFiles = false; 59 | $scope.queue = response.data.files || []; 60 | }, 61 | function () { 62 | $scope.loadingFiles = false; 63 | } 64 | ); 65 | } 66 | } 67 | ]) 68 | 69 | .controller('FileDestroyController', [ 70 | '$scope', '$http', 71 | function ($scope, $http) { 72 | var file = $scope.file, 73 | state; 74 | if (file.url) { 75 | file.$state = function () { 76 | return state; 77 | }; 78 | file.$destroy = function () { 79 | state = 'pending'; 80 | return $http({ 81 | url: file.deleteUrl, 82 | method: file.deleteType 83 | }).then( 84 | function () { 85 | state = 'resolved'; 86 | $scope.clear(file); 87 | }, 88 | function () { 89 | state = 'rejected'; 90 | } 91 | ); 92 | }; 93 | } else if (!file.$cancel && !file._index) { 94 | file.$cancel = function () { 95 | $scope.clear(file); 96 | }; 97 | } 98 | } 99 | ]); 100 | 101 | }()); 102 | -------------------------------------------------------------------------------- /static/js/cors/jquery.postmessage-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery postMessage Transport Plugin 1.1.1 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, window, document */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery'], factory); 19 | } else { 20 | // Browser globals: 21 | factory(window.jQuery); 22 | } 23 | }(function ($) { 24 | 'use strict'; 25 | 26 | var counter = 0, 27 | names = [ 28 | 'accepts', 29 | 'cache', 30 | 'contents', 31 | 'contentType', 32 | 'crossDomain', 33 | 'data', 34 | 'dataType', 35 | 'headers', 36 | 'ifModified', 37 | 'mimeType', 38 | 'password', 39 | 'processData', 40 | 'timeout', 41 | 'traditional', 42 | 'type', 43 | 'url', 44 | 'username' 45 | ], 46 | convert = function (p) { 47 | return p; 48 | }; 49 | 50 | $.ajaxSetup({ 51 | converters: { 52 | 'postmessage text': convert, 53 | 'postmessage json': convert, 54 | 'postmessage html': convert 55 | } 56 | }); 57 | 58 | $.ajaxTransport('postmessage', function (options) { 59 | if (options.postMessage && window.postMessage) { 60 | var iframe, 61 | loc = $('').prop('href', options.postMessage)[0], 62 | target = loc.protocol + '//' + loc.host, 63 | xhrUpload = options.xhr().upload; 64 | return { 65 | send: function (_, completeCallback) { 66 | counter += 1; 67 | var message = { 68 | id: 'postmessage-transport-' + counter 69 | }, 70 | eventName = 'message.' + message.id; 71 | iframe = $( 72 | '' 75 | ).bind('load', function () { 76 | $.each(names, function (i, name) { 77 | message[name] = options[name]; 78 | }); 79 | message.dataType = message.dataType.replace('postmessage ', ''); 80 | $(window).bind(eventName, function (e) { 81 | e = e.originalEvent; 82 | var data = e.data, 83 | ev; 84 | if (e.origin === target && data.id === message.id) { 85 | if (data.type === 'progress') { 86 | ev = document.createEvent('Event'); 87 | ev.initEvent(data.type, false, true); 88 | $.extend(ev, data); 89 | xhrUpload.dispatchEvent(ev); 90 | } else { 91 | completeCallback( 92 | data.status, 93 | data.statusText, 94 | {postmessage: data.result}, 95 | data.headers 96 | ); 97 | iframe.remove(); 98 | $(window).unbind(eventName); 99 | } 100 | } 101 | }); 102 | iframe[0].contentWindow.postMessage( 103 | message, 104 | target 105 | ); 106 | }).appendTo(document.body); 107 | }, 108 | abort: function () { 109 | if (iframe) { 110 | iframe.remove(); 111 | } 112 | } 113 | }; 114 | } 115 | }); 116 | 117 | })); 118 | -------------------------------------------------------------------------------- /static/js/cors/jquery.xdr-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery XDomainRequest Transport Plugin 1.1.3 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | * 11 | * Based on Julian Aubourg's ajaxHooks xdr.js: 12 | * https://github.com/jaubourg/ajaxHooks/ 13 | */ 14 | 15 | /* global define, window, XDomainRequest */ 16 | 17 | (function (factory) { 18 | 'use strict'; 19 | if (typeof define === 'function' && define.amd) { 20 | // Register as an anonymous AMD module: 21 | define(['jquery'], factory); 22 | } else { 23 | // Browser globals: 24 | factory(window.jQuery); 25 | } 26 | }(function ($) { 27 | 'use strict'; 28 | if (window.XDomainRequest && !$.support.cors) { 29 | $.ajaxTransport(function (s) { 30 | if (s.crossDomain && s.async) { 31 | if (s.timeout) { 32 | s.xdrTimeout = s.timeout; 33 | delete s.timeout; 34 | } 35 | var xdr; 36 | return { 37 | send: function (headers, completeCallback) { 38 | var addParamChar = /\?/.test(s.url) ? '&' : '?'; 39 | function callback(status, statusText, responses, responseHeaders) { 40 | xdr.onload = xdr.onerror = xdr.ontimeout = $.noop; 41 | xdr = null; 42 | completeCallback(status, statusText, responses, responseHeaders); 43 | } 44 | xdr = new XDomainRequest(); 45 | // XDomainRequest only supports GET and POST: 46 | if (s.type === 'DELETE') { 47 | s.url = s.url + addParamChar + '_method=DELETE'; 48 | s.type = 'POST'; 49 | } else if (s.type === 'PUT') { 50 | s.url = s.url + addParamChar + '_method=PUT'; 51 | s.type = 'POST'; 52 | } else if (s.type === 'PATCH') { 53 | s.url = s.url + addParamChar + '_method=PATCH'; 54 | s.type = 'POST'; 55 | } 56 | xdr.open(s.type, s.url); 57 | xdr.onload = function () { 58 | callback( 59 | 200, 60 | 'OK', 61 | {text: xdr.responseText}, 62 | 'Content-Type: ' + xdr.contentType 63 | ); 64 | }; 65 | xdr.onerror = function () { 66 | callback(404, 'Not Found'); 67 | }; 68 | if (s.xdrTimeout) { 69 | xdr.ontimeout = function () { 70 | callback(0, 'timeout'); 71 | }; 72 | xdr.timeout = s.xdrTimeout; 73 | } 74 | xdr.send((s.hasContent && s.data) || null); 75 | }, 76 | abort: function () { 77 | if (xdr) { 78 | xdr.onerror = $.noop(); 79 | xdr.abort(); 80 | } 81 | } 82 | }; 83 | } 84 | }); 85 | } 86 | })); 87 | -------------------------------------------------------------------------------- /static/js/jquery.fileupload-angular.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload AngularJS Plugin 2.2.0 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* jshint nomen:false */ 13 | /* global define, angular */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define([ 20 | 'jquery', 21 | 'angular', 22 | './jquery.fileupload-image', 23 | './jquery.fileupload-audio', 24 | './jquery.fileupload-video', 25 | './jquery.fileupload-validate' 26 | ], factory); 27 | } else { 28 | factory(); 29 | } 30 | }(function () { 31 | 'use strict'; 32 | 33 | angular.module('blueimp.fileupload', []) 34 | 35 | // The fileUpload service provides configuration options 36 | // for the fileUpload directive and default handlers for 37 | // File Upload events: 38 | .provider('fileUpload', function () { 39 | var scopeEvalAsync = function (expression) { 40 | var scope = angular.element(this) 41 | .fileupload('option', 'scope'); 42 | // Schedule a new $digest cycle if not already inside of one 43 | // and evaluate the given expression: 44 | scope.$evalAsync(expression); 45 | }, 46 | addFileMethods = function (scope, data) { 47 | var files = data.files, 48 | file = files[0]; 49 | angular.forEach(files, function (file, index) { 50 | file._index = index; 51 | file.$state = function () { 52 | return data.state(); 53 | }; 54 | file.$processing = function () { 55 | return data.processing(); 56 | }; 57 | file.$progress = function () { 58 | return data.progress(); 59 | }; 60 | file.$response = function () { 61 | return data.response(); 62 | }; 63 | }); 64 | file.$submit = function () { 65 | if (!file.error) { 66 | return data.submit(); 67 | } 68 | }; 69 | file.$cancel = function () { 70 | return data.abort(); 71 | }; 72 | }, 73 | $config; 74 | $config = this.defaults = { 75 | handleResponse: function (e, data) { 76 | var files = data.result && data.result.files; 77 | if (files) { 78 | data.scope.replace(data.files, files); 79 | } else if (data.errorThrown || 80 | data.textStatus === 'error') { 81 | data.files[0].error = data.errorThrown || 82 | data.textStatus; 83 | } 84 | }, 85 | add: function (e, data) { 86 | if (e.isDefaultPrevented()) { 87 | return false; 88 | } 89 | var scope = data.scope, 90 | filesCopy = []; 91 | angular.forEach(data.files, function (file) { 92 | filesCopy.push(file); 93 | }); 94 | scope.$apply(function () { 95 | addFileMethods(scope, data); 96 | var method = scope.option('prependFiles') ? 97 | 'unshift' : 'push'; 98 | Array.prototype[method].apply(scope.queue, data.files); 99 | }); 100 | data.process(function () { 101 | return scope.process(data); 102 | }).always(function () { 103 | scope.$apply(function () { 104 | addFileMethods(scope, data); 105 | scope.replace(filesCopy, data.files); 106 | }); 107 | }).then(function () { 108 | if ((scope.option('autoUpload') || 109 | data.autoUpload) && 110 | data.autoUpload !== false) { 111 | data.submit(); 112 | } 113 | }); 114 | }, 115 | progress: function (e, data) { 116 | if (e.isDefaultPrevented()) { 117 | return false; 118 | } 119 | data.scope.$apply(); 120 | }, 121 | done: function (e, data) { 122 | if (e.isDefaultPrevented()) { 123 | return false; 124 | } 125 | var that = this; 126 | data.scope.$apply(function () { 127 | data.handleResponse.call(that, e, data); 128 | }); 129 | }, 130 | fail: function (e, data) { 131 | if (e.isDefaultPrevented()) { 132 | return false; 133 | } 134 | var that = this, 135 | scope = data.scope; 136 | if (data.errorThrown === 'abort') { 137 | scope.clear(data.files); 138 | return; 139 | } 140 | scope.$apply(function () { 141 | data.handleResponse.call(that, e, data); 142 | }); 143 | }, 144 | stop: scopeEvalAsync, 145 | processstart: scopeEvalAsync, 146 | processstop: scopeEvalAsync, 147 | getNumberOfFiles: function () { 148 | var scope = this.scope; 149 | return scope.queue.length - scope.processing(); 150 | }, 151 | dataType: 'json', 152 | autoUpload: false 153 | }; 154 | this.$get = [ 155 | function () { 156 | return { 157 | defaults: $config 158 | }; 159 | } 160 | ]; 161 | }) 162 | 163 | // Format byte numbers to readable presentations: 164 | .provider('formatFileSizeFilter', function () { 165 | var $config = { 166 | // Byte units following the IEC format 167 | // http://en.wikipedia.org/wiki/Kilobyte 168 | units: [ 169 | {size: 1000000000, suffix: ' GB'}, 170 | {size: 1000000, suffix: ' MB'}, 171 | {size: 1000, suffix: ' KB'} 172 | ] 173 | }; 174 | this.defaults = $config; 175 | this.$get = function () { 176 | return function (bytes) { 177 | if (!angular.isNumber(bytes)) { 178 | return ''; 179 | } 180 | var unit = true, 181 | i = 0, 182 | prefix, 183 | suffix; 184 | while (unit) { 185 | unit = $config.units[i]; 186 | prefix = unit.prefix || ''; 187 | suffix = unit.suffix || ''; 188 | if (i === $config.units.length - 1 || bytes >= unit.size) { 189 | return prefix + (bytes / unit.size).toFixed(2) + suffix; 190 | } 191 | i += 1; 192 | } 193 | }; 194 | }; 195 | }) 196 | 197 | // The FileUploadController initializes the fileupload widget and 198 | // provides scope methods to control the File Upload functionality: 199 | .controller('FileUploadController', [ 200 | '$scope', '$element', '$attrs', '$window', 'fileUpload', 201 | function ($scope, $element, $attrs, $window, fileUpload) { 202 | var uploadMethods = { 203 | progress: function () { 204 | return $element.fileupload('progress'); 205 | }, 206 | active: function () { 207 | return $element.fileupload('active'); 208 | }, 209 | option: function (option, data) { 210 | if (arguments.length === 1) { 211 | return $element.fileupload('option', option); 212 | } 213 | $element.fileupload('option', option, data); 214 | }, 215 | add: function (data) { 216 | return $element.fileupload('add', data); 217 | }, 218 | send: function (data) { 219 | return $element.fileupload('send', data); 220 | }, 221 | process: function (data) { 222 | return $element.fileupload('process', data); 223 | }, 224 | processing: function (data) { 225 | return $element.fileupload('processing', data); 226 | } 227 | }; 228 | $scope.disabled = !$window.jQuery.support.fileInput; 229 | $scope.queue = $scope.queue || []; 230 | $scope.clear = function (files) { 231 | var queue = this.queue, 232 | i = queue.length, 233 | file = files, 234 | length = 1; 235 | if (angular.isArray(files)) { 236 | file = files[0]; 237 | length = files.length; 238 | } 239 | while (i) { 240 | i -= 1; 241 | if (queue[i] === file) { 242 | return queue.splice(i, length); 243 | } 244 | } 245 | }; 246 | $scope.replace = function (oldFiles, newFiles) { 247 | var queue = this.queue, 248 | file = oldFiles[0], 249 | i, 250 | j; 251 | for (i = 0; i < queue.length; i += 1) { 252 | if (queue[i] === file) { 253 | for (j = 0; j < newFiles.length; j += 1) { 254 | queue[i + j] = newFiles[j]; 255 | } 256 | return; 257 | } 258 | } 259 | }; 260 | $scope.applyOnQueue = function (method) { 261 | var list = this.queue.slice(0), 262 | i, 263 | file; 264 | for (i = 0; i < list.length; i += 1) { 265 | file = list[i]; 266 | if (file[method]) { 267 | file[method](); 268 | } 269 | } 270 | }; 271 | $scope.submit = function () { 272 | this.applyOnQueue('$submit'); 273 | }; 274 | $scope.cancel = function () { 275 | this.applyOnQueue('$cancel'); 276 | }; 277 | // Add upload methods to the scope: 278 | angular.extend($scope, uploadMethods); 279 | // The fileupload widget will initialize with 280 | // the options provided via "data-"-parameters, 281 | // as well as those given via options object: 282 | $element.fileupload(angular.extend( 283 | {scope: $scope}, 284 | fileUpload.defaults 285 | )).on('fileuploadadd', function (e, data) { 286 | data.scope = $scope; 287 | }).on('fileuploadfail', function (e, data) { 288 | if (data.errorThrown === 'abort') { 289 | return; 290 | } 291 | if (data.dataType && 292 | data.dataType.indexOf('json') === data.dataType.length - 4) { 293 | try { 294 | data.result = angular.fromJson(data.jqXHR.responseText); 295 | } catch (ignore) {} 296 | } 297 | }).on([ 298 | 'fileuploadadd', 299 | 'fileuploadsubmit', 300 | 'fileuploadsend', 301 | 'fileuploaddone', 302 | 'fileuploadfail', 303 | 'fileuploadalways', 304 | 'fileuploadprogress', 305 | 'fileuploadprogressall', 306 | 'fileuploadstart', 307 | 'fileuploadstop', 308 | 'fileuploadchange', 309 | 'fileuploadpaste', 310 | 'fileuploaddrop', 311 | 'fileuploaddragover', 312 | 'fileuploadchunksend', 313 | 'fileuploadchunkdone', 314 | 'fileuploadchunkfail', 315 | 'fileuploadchunkalways', 316 | 'fileuploadprocessstart', 317 | 'fileuploadprocess', 318 | 'fileuploadprocessdone', 319 | 'fileuploadprocessfail', 320 | 'fileuploadprocessalways', 321 | 'fileuploadprocessstop' 322 | ].join(' '), function (e, data) { 323 | if ($scope.$emit(e.type, data).defaultPrevented) { 324 | e.preventDefault(); 325 | } 326 | }).on('remove', function () { 327 | // Remove upload methods from the scope, 328 | // when the widget is removed: 329 | var method; 330 | for (method in uploadMethods) { 331 | if (uploadMethods.hasOwnProperty(method)) { 332 | delete $scope[method]; 333 | } 334 | } 335 | }); 336 | // Observe option changes: 337 | $scope.$watch( 338 | $attrs.fileUpload, 339 | function (newOptions) { 340 | if (newOptions) { 341 | $element.fileupload('option', newOptions); 342 | } 343 | } 344 | ); 345 | } 346 | ]) 347 | 348 | // Provide File Upload progress feedback: 349 | .controller('FileUploadProgressController', [ 350 | '$scope', '$attrs', '$parse', 351 | function ($scope, $attrs, $parse) { 352 | var fn = $parse($attrs.fileUploadProgress), 353 | update = function () { 354 | var progress = fn($scope); 355 | if (!progress || !progress.total) { 356 | return; 357 | } 358 | $scope.num = Math.floor( 359 | progress.loaded / progress.total * 100 360 | ); 361 | }; 362 | update(); 363 | $scope.$watch( 364 | $attrs.fileUploadProgress + '.loaded', 365 | function (newValue, oldValue) { 366 | if (newValue !== oldValue) { 367 | update(); 368 | } 369 | } 370 | ); 371 | } 372 | ]) 373 | 374 | // Display File Upload previews: 375 | .controller('FileUploadPreviewController', [ 376 | '$scope', '$element', '$attrs', 377 | function ($scope, $element, $attrs) { 378 | $scope.$watch( 379 | $attrs.fileUploadPreview + '.preview', 380 | function (preview) { 381 | $element.empty(); 382 | if (preview) { 383 | $element.append(preview); 384 | } 385 | } 386 | ); 387 | } 388 | ]) 389 | 390 | .directive('fileUpload', function () { 391 | return { 392 | controller: 'FileUploadController', 393 | scope: true 394 | }; 395 | }) 396 | 397 | .directive('fileUploadProgress', function () { 398 | return { 399 | controller: 'FileUploadProgressController', 400 | scope: true 401 | }; 402 | }) 403 | 404 | .directive('fileUploadPreview', function () { 405 | return { 406 | controller: 'FileUploadPreviewController' 407 | }; 408 | }) 409 | 410 | // Enhance the HTML5 download attribute to 411 | // allow drag&drop of files to the desktop: 412 | .directive('download', function () { 413 | return function (scope, elm) { 414 | elm.on('dragstart', function (e) { 415 | try { 416 | e.originalEvent.dataTransfer.setData( 417 | 'DownloadURL', 418 | [ 419 | 'application/octet-stream', 420 | elm.prop('download'), 421 | elm.prop('href') 422 | ].join(':') 423 | ); 424 | } catch (ignore) {} 425 | }); 426 | }; 427 | }); 428 | 429 | })); 430 | -------------------------------------------------------------------------------- /static/js/jquery.fileupload-audio.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Audio Preview Plugin 1.0.3 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* jshint nomen:false */ 13 | /* global define, window, document */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define([ 20 | 'jquery', 21 | 'load-image', 22 | './jquery.fileupload-process' 23 | ], factory); 24 | } else { 25 | // Browser globals: 26 | factory( 27 | window.jQuery, 28 | window.loadImage 29 | ); 30 | } 31 | }(function ($, loadImage) { 32 | 'use strict'; 33 | 34 | // Prepend to the default processQueue: 35 | $.blueimp.fileupload.prototype.options.processQueue.unshift( 36 | { 37 | action: 'loadAudio', 38 | // Use the action as prefix for the "@" options: 39 | prefix: true, 40 | fileTypes: '@', 41 | maxFileSize: '@', 42 | disabled: '@disableAudioPreview' 43 | }, 44 | { 45 | action: 'setAudio', 46 | name: '@audioPreviewName', 47 | disabled: '@disableAudioPreview' 48 | } 49 | ); 50 | 51 | // The File Upload Audio Preview plugin extends the fileupload widget 52 | // with audio preview functionality: 53 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 54 | 55 | options: { 56 | // The regular expression for the types of audio files to load, 57 | // matched against the file type: 58 | loadAudioFileTypes: /^audio\/.*$/ 59 | }, 60 | 61 | _audioElement: document.createElement('audio'), 62 | 63 | processActions: { 64 | 65 | // Loads the audio file given via data.files and data.index 66 | // as audio element if the browser supports playing it. 67 | // Accepts the options fileTypes (regular expression) 68 | // and maxFileSize (integer) to limit the files to load: 69 | loadAudio: function (data, options) { 70 | if (options.disabled) { 71 | return data; 72 | } 73 | var file = data.files[data.index], 74 | url, 75 | audio; 76 | if (this._audioElement.canPlayType && 77 | this._audioElement.canPlayType(file.type) && 78 | ($.type(options.maxFileSize) !== 'number' || 79 | file.size <= options.maxFileSize) && 80 | (!options.fileTypes || 81 | options.fileTypes.test(file.type))) { 82 | url = loadImage.createObjectURL(file); 83 | if (url) { 84 | audio = this._audioElement.cloneNode(false); 85 | audio.src = url; 86 | audio.controls = true; 87 | data.audio = audio; 88 | return data; 89 | } 90 | } 91 | return data; 92 | }, 93 | 94 | // Sets the audio element as a property of the file object: 95 | setAudio: function (data, options) { 96 | if (data.audio && !options.disabled) { 97 | data.files[data.index][options.name || 'preview'] = data.audio; 98 | } 99 | return data; 100 | } 101 | 102 | } 103 | 104 | }); 105 | 106 | })); 107 | -------------------------------------------------------------------------------- /static/js/jquery.fileupload-image.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Image Preview & Resize Plugin 1.7.2 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* jshint nomen:false */ 13 | /* global define, window, Blob */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define([ 20 | 'jquery', 21 | 'load-image', 22 | 'load-image-meta', 23 | 'load-image-exif', 24 | 'load-image-ios', 25 | 'canvas-to-blob', 26 | './jquery.fileupload-process' 27 | ], factory); 28 | } else { 29 | // Browser globals: 30 | factory( 31 | window.jQuery, 32 | window.loadImage 33 | ); 34 | } 35 | }(function ($, loadImage) { 36 | 'use strict'; 37 | 38 | // Prepend to the default processQueue: 39 | $.blueimp.fileupload.prototype.options.processQueue.unshift( 40 | { 41 | action: 'loadImageMetaData', 42 | disableImageHead: '@', 43 | disableExif: '@', 44 | disableExifThumbnail: '@', 45 | disableExifSub: '@', 46 | disableExifGps: '@', 47 | disabled: '@disableImageMetaDataLoad' 48 | }, 49 | { 50 | action: 'loadImage', 51 | // Use the action as prefix for the "@" options: 52 | prefix: true, 53 | fileTypes: '@', 54 | maxFileSize: '@', 55 | noRevoke: '@', 56 | disabled: '@disableImageLoad' 57 | }, 58 | { 59 | action: 'resizeImage', 60 | // Use "image" as prefix for the "@" options: 61 | prefix: 'image', 62 | maxWidth: '@', 63 | maxHeight: '@', 64 | minWidth: '@', 65 | minHeight: '@', 66 | crop: '@', 67 | orientation: '@', 68 | forceResize: '@', 69 | disabled: '@disableImageResize' 70 | }, 71 | { 72 | action: 'saveImage', 73 | quality: '@imageQuality', 74 | type: '@imageType', 75 | disabled: '@disableImageResize' 76 | }, 77 | { 78 | action: 'saveImageMetaData', 79 | disabled: '@disableImageMetaDataSave' 80 | }, 81 | { 82 | action: 'resizeImage', 83 | // Use "preview" as prefix for the "@" options: 84 | prefix: 'preview', 85 | maxWidth: '@', 86 | maxHeight: '@', 87 | minWidth: '@', 88 | minHeight: '@', 89 | crop: '@', 90 | orientation: '@', 91 | thumbnail: '@', 92 | canvas: '@', 93 | disabled: '@disableImagePreview' 94 | }, 95 | { 96 | action: 'setImage', 97 | name: '@imagePreviewName', 98 | disabled: '@disableImagePreview' 99 | }, 100 | { 101 | action: 'deleteImageReferences', 102 | disabled: '@disableImageReferencesDeletion' 103 | } 104 | ); 105 | 106 | // The File Upload Resize plugin extends the fileupload widget 107 | // with image resize functionality: 108 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 109 | 110 | options: { 111 | // The regular expression for the types of images to load: 112 | // matched against the file type: 113 | loadImageFileTypes: /^image\/(gif|jpeg|png|svg\+xml)$/, 114 | // The maximum file size of images to load: 115 | loadImageMaxFileSize: 10000000, // 10MB 116 | // The maximum width of resized images: 117 | imageMaxWidth: 1920, 118 | // The maximum height of resized images: 119 | imageMaxHeight: 1080, 120 | // Defines the image orientation (1-8) or takes the orientation 121 | // value from Exif data if set to true: 122 | imageOrientation: false, 123 | // Define if resized images should be cropped or only scaled: 124 | imageCrop: false, 125 | // Disable the resize image functionality by default: 126 | disableImageResize: true, 127 | // The maximum width of the preview images: 128 | previewMaxWidth: 80, 129 | // The maximum height of the preview images: 130 | previewMaxHeight: 80, 131 | // Defines the preview orientation (1-8) or takes the orientation 132 | // value from Exif data if set to true: 133 | previewOrientation: true, 134 | // Create the preview using the Exif data thumbnail: 135 | previewThumbnail: true, 136 | // Define if preview images should be cropped or only scaled: 137 | previewCrop: false, 138 | // Define if preview images should be resized as canvas elements: 139 | previewCanvas: true 140 | }, 141 | 142 | processActions: { 143 | 144 | // Loads the image given via data.files and data.index 145 | // as img element, if the browser supports the File API. 146 | // Accepts the options fileTypes (regular expression) 147 | // and maxFileSize (integer) to limit the files to load: 148 | loadImage: function (data, options) { 149 | if (options.disabled) { 150 | return data; 151 | } 152 | var that = this, 153 | file = data.files[data.index], 154 | dfd = $.Deferred(); 155 | if (($.type(options.maxFileSize) === 'number' && 156 | file.size > options.maxFileSize) || 157 | (options.fileTypes && 158 | !options.fileTypes.test(file.type)) || 159 | !loadImage( 160 | file, 161 | function (img) { 162 | if (img.src) { 163 | data.img = img; 164 | } 165 | dfd.resolveWith(that, [data]); 166 | }, 167 | options 168 | )) { 169 | return data; 170 | } 171 | return dfd.promise(); 172 | }, 173 | 174 | // Resizes the image given as data.canvas or data.img 175 | // and updates data.canvas or data.img with the resized image. 176 | // Also stores the resized image as preview property. 177 | // Accepts the options maxWidth, maxHeight, minWidth, 178 | // minHeight, canvas and crop: 179 | resizeImage: function (data, options) { 180 | if (options.disabled || !(data.canvas || data.img)) { 181 | return data; 182 | } 183 | options = $.extend({canvas: true}, options); 184 | var that = this, 185 | dfd = $.Deferred(), 186 | img = (options.canvas && data.canvas) || data.img, 187 | resolve = function (newImg) { 188 | if (newImg && (newImg.width !== img.width || 189 | newImg.height !== img.height || 190 | options.forceResize)) { 191 | data[newImg.getContext ? 'canvas' : 'img'] = newImg; 192 | } 193 | data.preview = newImg; 194 | dfd.resolveWith(that, [data]); 195 | }, 196 | thumbnail; 197 | if (data.exif) { 198 | if (options.orientation === true) { 199 | options.orientation = data.exif.get('Orientation'); 200 | } 201 | if (options.thumbnail) { 202 | thumbnail = data.exif.get('Thumbnail'); 203 | if (thumbnail) { 204 | loadImage(thumbnail, resolve, options); 205 | return dfd.promise(); 206 | } 207 | } 208 | // Prevent orienting the same image twice: 209 | if (data.orientation) { 210 | delete options.orientation; 211 | } else { 212 | data.orientation = options.orientation; 213 | } 214 | } 215 | if (img) { 216 | resolve(loadImage.scale(img, options)); 217 | return dfd.promise(); 218 | } 219 | return data; 220 | }, 221 | 222 | // Saves the processed image given as data.canvas 223 | // inplace at data.index of data.files: 224 | saveImage: function (data, options) { 225 | if (!data.canvas || options.disabled) { 226 | return data; 227 | } 228 | var that = this, 229 | file = data.files[data.index], 230 | dfd = $.Deferred(); 231 | if (data.canvas.toBlob) { 232 | data.canvas.toBlob( 233 | function (blob) { 234 | if (!blob.name) { 235 | if (file.type === blob.type) { 236 | blob.name = file.name; 237 | } else if (file.name) { 238 | blob.name = file.name.replace( 239 | /\..+$/, 240 | '.' + blob.type.substr(6) 241 | ); 242 | } 243 | } 244 | // Don't restore invalid meta data: 245 | if (file.type !== blob.type) { 246 | delete data.imageHead; 247 | } 248 | // Store the created blob at the position 249 | // of the original file in the files list: 250 | data.files[data.index] = blob; 251 | dfd.resolveWith(that, [data]); 252 | }, 253 | options.type || file.type, 254 | options.quality 255 | ); 256 | } else { 257 | return data; 258 | } 259 | return dfd.promise(); 260 | }, 261 | 262 | loadImageMetaData: function (data, options) { 263 | if (options.disabled) { 264 | return data; 265 | } 266 | var that = this, 267 | dfd = $.Deferred(); 268 | loadImage.parseMetaData(data.files[data.index], function (result) { 269 | $.extend(data, result); 270 | dfd.resolveWith(that, [data]); 271 | }, options); 272 | return dfd.promise(); 273 | }, 274 | 275 | saveImageMetaData: function (data, options) { 276 | if (!(data.imageHead && data.canvas && 277 | data.canvas.toBlob && !options.disabled)) { 278 | return data; 279 | } 280 | var file = data.files[data.index], 281 | blob = new Blob([ 282 | data.imageHead, 283 | // Resized images always have a head size of 20 bytes, 284 | // including the JPEG marker and a minimal JFIF header: 285 | this._blobSlice.call(file, 20) 286 | ], {type: file.type}); 287 | blob.name = file.name; 288 | data.files[data.index] = blob; 289 | return data; 290 | }, 291 | 292 | // Sets the resized version of the image as a property of the 293 | // file object, must be called after "saveImage": 294 | setImage: function (data, options) { 295 | if (data.preview && !options.disabled) { 296 | data.files[data.index][options.name || 'preview'] = data.preview; 297 | } 298 | return data; 299 | }, 300 | 301 | deleteImageReferences: function (data, options) { 302 | if (!options.disabled) { 303 | delete data.img; 304 | delete data.canvas; 305 | delete data.preview; 306 | delete data.imageHead; 307 | } 308 | return data; 309 | } 310 | 311 | } 312 | 313 | }); 314 | 315 | })); 316 | -------------------------------------------------------------------------------- /static/js/jquery.fileupload-jquery-ui.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload jQuery UI Plugin 8.7.1 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* jshint nomen:false */ 13 | /* global define, window */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define(['jquery', './jquery.fileupload-ui'], factory); 20 | } else { 21 | // Browser globals: 22 | factory(window.jQuery); 23 | } 24 | }(function ($) { 25 | 'use strict'; 26 | 27 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 28 | 29 | options: { 30 | processdone: function (e, data) { 31 | data.context.find('.start').button('enable'); 32 | }, 33 | progress: function (e, data) { 34 | if (data.context) { 35 | data.context.find('.progress').progressbar( 36 | 'option', 37 | 'value', 38 | parseInt(data.loaded / data.total * 100, 10) 39 | ); 40 | } 41 | }, 42 | progressall: function (e, data) { 43 | var $this = $(this); 44 | $this.find('.fileupload-progress') 45 | .find('.progress').progressbar( 46 | 'option', 47 | 'value', 48 | parseInt(data.loaded / data.total * 100, 10) 49 | ).end() 50 | .find('.progress-extended').each(function () { 51 | $(this).html( 52 | ($this.data('blueimp-fileupload') || 53 | $this.data('fileupload')) 54 | ._renderExtendedProgress(data) 55 | ); 56 | }); 57 | } 58 | }, 59 | 60 | _renderUpload: function (func, files) { 61 | var node = this._super(func, files), 62 | showIconText = $(window).width() > 480; 63 | node.find('.progress').empty().progressbar(); 64 | node.find('.start').button({ 65 | icons: {primary: 'ui-icon-circle-arrow-e'}, 66 | text: showIconText 67 | }); 68 | node.find('.cancel').button({ 69 | icons: {primary: 'ui-icon-cancel'}, 70 | text: showIconText 71 | }); 72 | if (node.hasClass('fade')) { 73 | node.hide(); 74 | } 75 | return node; 76 | }, 77 | 78 | _renderDownload: function (func, files) { 79 | var node = this._super(func, files), 80 | showIconText = $(window).width() > 480; 81 | node.find('.delete').button({ 82 | icons: {primary: 'ui-icon-trash'}, 83 | text: showIconText 84 | }); 85 | if (node.hasClass('fade')) { 86 | node.hide(); 87 | } 88 | return node; 89 | }, 90 | 91 | _startHandler: function (e) { 92 | $(e.currentTarget).button('disable'); 93 | this._super(e); 94 | }, 95 | 96 | _transition: function (node) { 97 | var deferred = $.Deferred(); 98 | if (node.hasClass('fade')) { 99 | node.fadeToggle( 100 | this.options.transitionDuration, 101 | this.options.transitionEasing, 102 | function () { 103 | deferred.resolveWith(node); 104 | } 105 | ); 106 | } else { 107 | deferred.resolveWith(node); 108 | } 109 | return deferred; 110 | }, 111 | 112 | _create: function () { 113 | this._super(); 114 | this.element 115 | .find('.fileupload-buttonbar') 116 | .find('.fileinput-button').each(function () { 117 | var input = $(this).find('input:file').detach(); 118 | $(this) 119 | .button({icons: {primary: 'ui-icon-plusthick'}}) 120 | .append(input); 121 | }) 122 | .end().find('.start') 123 | .button({icons: {primary: 'ui-icon-circle-arrow-e'}}) 124 | .end().find('.cancel') 125 | .button({icons: {primary: 'ui-icon-cancel'}}) 126 | .end().find('.delete') 127 | .button({icons: {primary: 'ui-icon-trash'}}) 128 | .end().find('.progress').progressbar(); 129 | }, 130 | 131 | _destroy: function () { 132 | this.element 133 | .find('.fileupload-buttonbar') 134 | .find('.fileinput-button').each(function () { 135 | var input = $(this).find('input:file').detach(); 136 | $(this) 137 | .button('destroy') 138 | .append(input); 139 | }) 140 | .end().find('.start') 141 | .button('destroy') 142 | .end().find('.cancel') 143 | .button('destroy') 144 | .end().find('.delete') 145 | .button('destroy') 146 | .end().find('.progress').progressbar('destroy'); 147 | this._super(); 148 | } 149 | 150 | }); 151 | 152 | })); 153 | -------------------------------------------------------------------------------- /static/js/jquery.fileupload-process.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Processing Plugin 1.3.0 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2012, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* jshint nomen:false */ 13 | /* global define, window */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define([ 20 | 'jquery', 21 | './jquery.fileupload' 22 | ], factory); 23 | } else { 24 | // Browser globals: 25 | factory( 26 | window.jQuery 27 | ); 28 | } 29 | }(function ($) { 30 | 'use strict'; 31 | 32 | var originalAdd = $.blueimp.fileupload.prototype.options.add; 33 | 34 | // The File Upload Processing plugin extends the fileupload widget 35 | // with file processing functionality: 36 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 37 | 38 | options: { 39 | // The list of processing actions: 40 | processQueue: [ 41 | /* 42 | { 43 | action: 'log', 44 | type: 'debug' 45 | } 46 | */ 47 | ], 48 | add: function (e, data) { 49 | var $this = $(this); 50 | data.process(function () { 51 | return $this.fileupload('process', data); 52 | }); 53 | originalAdd.call(this, e, data); 54 | } 55 | }, 56 | 57 | processActions: { 58 | /* 59 | log: function (data, options) { 60 | console[options.type]( 61 | 'Processing "' + data.files[data.index].name + '"' 62 | ); 63 | } 64 | */ 65 | }, 66 | 67 | _processFile: function (data, originalData) { 68 | var that = this, 69 | dfd = $.Deferred().resolveWith(that, [data]), 70 | chain = dfd.promise(); 71 | this._trigger('process', null, data); 72 | $.each(data.processQueue, function (i, settings) { 73 | var func = function (data) { 74 | if (originalData.errorThrown) { 75 | return $.Deferred() 76 | .rejectWith(that, [originalData]).promise(); 77 | } 78 | return that.processActions[settings.action].call( 79 | that, 80 | data, 81 | settings 82 | ); 83 | }; 84 | chain = chain.pipe(func, settings.always && func); 85 | }); 86 | chain 87 | .done(function () { 88 | that._trigger('processdone', null, data); 89 | that._trigger('processalways', null, data); 90 | }) 91 | .fail(function () { 92 | that._trigger('processfail', null, data); 93 | that._trigger('processalways', null, data); 94 | }); 95 | return chain; 96 | }, 97 | 98 | // Replaces the settings of each processQueue item that 99 | // are strings starting with an "@", using the remaining 100 | // substring as key for the option map, 101 | // e.g. "@autoUpload" is replaced with options.autoUpload: 102 | _transformProcessQueue: function (options) { 103 | var processQueue = []; 104 | $.each(options.processQueue, function () { 105 | var settings = {}, 106 | action = this.action, 107 | prefix = this.prefix === true ? action : this.prefix; 108 | $.each(this, function (key, value) { 109 | if ($.type(value) === 'string' && 110 | value.charAt(0) === '@') { 111 | settings[key] = options[ 112 | value.slice(1) || (prefix ? prefix + 113 | key.charAt(0).toUpperCase() + key.slice(1) : key) 114 | ]; 115 | } else { 116 | settings[key] = value; 117 | } 118 | 119 | }); 120 | processQueue.push(settings); 121 | }); 122 | options.processQueue = processQueue; 123 | }, 124 | 125 | // Returns the number of files currently in the processsing queue: 126 | processing: function () { 127 | return this._processing; 128 | }, 129 | 130 | // Processes the files given as files property of the data parameter, 131 | // returns a Promise object that allows to bind callbacks: 132 | process: function (data) { 133 | var that = this, 134 | options = $.extend({}, this.options, data); 135 | if (options.processQueue && options.processQueue.length) { 136 | this._transformProcessQueue(options); 137 | if (this._processing === 0) { 138 | this._trigger('processstart'); 139 | } 140 | $.each(data.files, function (index) { 141 | var opts = index ? $.extend({}, options) : options, 142 | func = function () { 143 | if (data.errorThrown) { 144 | return $.Deferred() 145 | .rejectWith(that, [data]).promise(); 146 | } 147 | return that._processFile(opts, data); 148 | }; 149 | opts.index = index; 150 | that._processing += 1; 151 | that._processingQueue = that._processingQueue.pipe(func, func) 152 | .always(function () { 153 | that._processing -= 1; 154 | if (that._processing === 0) { 155 | that._trigger('processstop'); 156 | } 157 | }); 158 | }); 159 | } 160 | return this._processingQueue; 161 | }, 162 | 163 | _create: function () { 164 | this._super(); 165 | this._processing = 0; 166 | this._processingQueue = $.Deferred().resolveWith(this) 167 | .promise(); 168 | } 169 | 170 | }); 171 | 172 | })); 173 | -------------------------------------------------------------------------------- /static/js/jquery.fileupload-ui.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload User Interface Plugin 9.6.0 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* jshint nomen:false */ 13 | /* global define, window */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define([ 20 | 'jquery', 21 | 'tmpl', 22 | './jquery.fileupload-image', 23 | './jquery.fileupload-audio', 24 | './jquery.fileupload-video', 25 | './jquery.fileupload-validate' 26 | ], factory); 27 | } else { 28 | // Browser globals: 29 | factory( 30 | window.jQuery, 31 | window.tmpl 32 | ); 33 | } 34 | }(function ($, tmpl) { 35 | 'use strict'; 36 | 37 | $.blueimp.fileupload.prototype._specialOptions.push( 38 | 'filesContainer', 39 | 'uploadTemplateId', 40 | 'downloadTemplateId' 41 | ); 42 | 43 | // The UI version extends the file upload widget 44 | // and adds complete user interface interaction: 45 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 46 | 47 | options: { 48 | // By default, files added to the widget are uploaded as soon 49 | // as the user clicks on the start buttons. To enable automatic 50 | // uploads, set the following option to true: 51 | autoUpload: false, 52 | // The ID of the upload template: 53 | uploadTemplateId: 'template-upload', 54 | // The ID of the download template: 55 | downloadTemplateId: 'template-download', 56 | // The container for the list of files. If undefined, it is set to 57 | // an element with class "files" inside of the widget element: 58 | filesContainer: undefined, 59 | // By default, files are appended to the files container. 60 | // Set the following option to true, to prepend files instead: 61 | prependFiles: false, 62 | // The expected data type of the upload response, sets the dataType 63 | // option of the $.ajax upload requests: 64 | dataType: 'json', 65 | 66 | // Error and info messages: 67 | messages: { 68 | unknownError: 'Unknown error' 69 | }, 70 | 71 | // Function returning the current number of files, 72 | // used by the maxNumberOfFiles validation: 73 | getNumberOfFiles: function () { 74 | return this.filesContainer.children() 75 | .not('.processing').length; 76 | }, 77 | 78 | // Callback to retrieve the list of files from the server response: 79 | getFilesFromResponse: function (data) { 80 | if (data.result && $.isArray(data.result.files)) { 81 | return data.result.files; 82 | } 83 | return []; 84 | }, 85 | 86 | // The add callback is invoked as soon as files are added to the fileupload 87 | // widget (via file input selection, drag & drop or add API call). 88 | // See the basic file upload widget for more information: 89 | add: function (e, data) { 90 | if (e.isDefaultPrevented()) { 91 | return false; 92 | } 93 | var $this = $(this), 94 | that = $this.data('blueimp-fileupload') || 95 | $this.data('fileupload'), 96 | options = that.options; 97 | data.context = that._renderUpload(data.files) 98 | .data('data', data) 99 | .addClass('processing'); 100 | options.filesContainer[ 101 | options.prependFiles ? 'prepend' : 'append' 102 | ](data.context); 103 | that._forceReflow(data.context); 104 | that._transition(data.context); 105 | data.process(function () { 106 | return $this.fileupload('process', data); 107 | }).always(function () { 108 | data.context.each(function (index) { 109 | $(this).find('.size').text( 110 | that._formatFileSize(data.files[index].size) 111 | ); 112 | }).removeClass('processing'); 113 | that._renderPreviews(data); 114 | }).done(function () { 115 | data.context.find('.start').prop('disabled', false); 116 | if ((that._trigger('added', e, data) !== false) && 117 | (options.autoUpload || data.autoUpload) && 118 | data.autoUpload !== false) { 119 | data.submit(); 120 | } 121 | }).fail(function () { 122 | if (data.files.error) { 123 | data.context.each(function (index) { 124 | var error = data.files[index].error; 125 | if (error) { 126 | $(this).find('.error').text(error); 127 | } 128 | }); 129 | } 130 | }); 131 | }, 132 | // Callback for the start of each file upload request: 133 | send: function (e, data) { 134 | if (e.isDefaultPrevented()) { 135 | return false; 136 | } 137 | var that = $(this).data('blueimp-fileupload') || 138 | $(this).data('fileupload'); 139 | if (data.context && data.dataType && 140 | data.dataType.substr(0, 6) === 'iframe') { 141 | // Iframe Transport does not support progress events. 142 | // In lack of an indeterminate progress bar, we set 143 | // the progress to 100%, showing the full animated bar: 144 | data.context 145 | .find('.progress').addClass( 146 | !$.support.transition && 'progress-animated' 147 | ) 148 | .attr('aria-valuenow', 100) 149 | .children().first().css( 150 | 'width', 151 | '100%' 152 | ); 153 | } 154 | return that._trigger('sent', e, data); 155 | }, 156 | // Callback for successful uploads: 157 | done: function (e, data) { 158 | if (e.isDefaultPrevented()) { 159 | return false; 160 | } 161 | var that = $(this).data('blueimp-fileupload') || 162 | $(this).data('fileupload'), 163 | getFilesFromResponse = data.getFilesFromResponse || 164 | that.options.getFilesFromResponse, 165 | files = getFilesFromResponse(data), 166 | template, 167 | deferred; 168 | // console.log(data); 169 | if (data.context) { 170 | data.context.each(function (index) { 171 | var file = files[index] || 172 | {error: 'Empty file upload result'}; 173 | deferred = that._addFinishedDeferreds(); 174 | that._transition($(this)).done( 175 | function () { 176 | var node = $(this); 177 | template = that._renderDownload([file]) 178 | .replaceAll(node); 179 | that._forceReflow(template); 180 | that._transition(template).done( 181 | function () { 182 | data.context = $(this); 183 | that._trigger('completed', e, data); 184 | that._trigger('finished', e, data); 185 | deferred.resolve(); 186 | } 187 | ); 188 | } 189 | ); 190 | }); 191 | } else { 192 | template = that._renderDownload(files)[ 193 | that.options.prependFiles ? 'prependTo' : 'appendTo' 194 | ](that.options.filesContainer); 195 | that._forceReflow(template); 196 | deferred = that._addFinishedDeferreds(); 197 | that._transition(template).done( 198 | function () { 199 | data.context = $(this); 200 | that._trigger('completed', e, data); 201 | that._trigger('finished', e, data); 202 | deferred.resolve(); 203 | } 204 | ); 205 | } 206 | }, 207 | // Callback for failed (abort or error) uploads: 208 | fail: function (e, data) { 209 | if (e.isDefaultPrevented()) { 210 | return false; 211 | } 212 | var that = $(this).data('blueimp-fileupload') || 213 | $(this).data('fileupload'), 214 | template, 215 | deferred; 216 | if (data.context) { 217 | data.context.each(function (index) { 218 | if (data.errorThrown !== 'abort') { 219 | var file = data.files[index]; 220 | file.error = file.error || data.errorThrown || 221 | data.i18n('unknownError'); 222 | deferred = that._addFinishedDeferreds(); 223 | that._transition($(this)).done( 224 | function () { 225 | var node = $(this); 226 | template = that._renderDownload([file]) 227 | .replaceAll(node); 228 | that._forceReflow(template); 229 | that._transition(template).done( 230 | function () { 231 | data.context = $(this); 232 | that._trigger('failed', e, data); 233 | that._trigger('finished', e, data); 234 | deferred.resolve(); 235 | } 236 | ); 237 | } 238 | ); 239 | } else { 240 | deferred = that._addFinishedDeferreds(); 241 | that._transition($(this)).done( 242 | function () { 243 | $(this).remove(); 244 | that._trigger('failed', e, data); 245 | that._trigger('finished', e, data); 246 | deferred.resolve(); 247 | } 248 | ); 249 | } 250 | }); 251 | } else if (data.errorThrown !== 'abort') { 252 | data.context = that._renderUpload(data.files)[ 253 | that.options.prependFiles ? 'prependTo' : 'appendTo' 254 | ](that.options.filesContainer) 255 | .data('data', data); 256 | that._forceReflow(data.context); 257 | deferred = that._addFinishedDeferreds(); 258 | that._transition(data.context).done( 259 | function () { 260 | data.context = $(this); 261 | that._trigger('failed', e, data); 262 | that._trigger('finished', e, data); 263 | deferred.resolve(); 264 | } 265 | ); 266 | } else { 267 | that._trigger('failed', e, data); 268 | that._trigger('finished', e, data); 269 | that._addFinishedDeferreds().resolve(); 270 | } 271 | }, 272 | // Callback for upload progress events: 273 | progress: function (e, data) { 274 | if (e.isDefaultPrevented()) { 275 | return false; 276 | } 277 | var progress = Math.floor(data.loaded / data.total * 100); 278 | if (data.context) { 279 | data.context.each(function () { 280 | $(this).find('.progress') 281 | .attr('aria-valuenow', progress) 282 | .children().first().css( 283 | 'width', 284 | progress + '%' 285 | ); 286 | }); 287 | } 288 | }, 289 | // Callback for global upload progress events: 290 | progressall: function (e, data) { 291 | if (e.isDefaultPrevented()) { 292 | return false; 293 | } 294 | var $this = $(this), 295 | progress = Math.floor(data.loaded / data.total * 100), 296 | globalProgressNode = $this.find('.fileupload-progress'), 297 | extendedProgressNode = globalProgressNode 298 | .find('.progress-extended'); 299 | if (extendedProgressNode.length) { 300 | extendedProgressNode.html( 301 | ($this.data('blueimp-fileupload') || $this.data('fileupload')) 302 | ._renderExtendedProgress(data) 303 | ); 304 | } 305 | globalProgressNode 306 | .find('.progress') 307 | .attr('aria-valuenow', progress) 308 | .children().first().css( 309 | 'width', 310 | progress + '%' 311 | ); 312 | }, 313 | // Callback for uploads start, equivalent to the global ajaxStart event: 314 | start: function (e) { 315 | if (e.isDefaultPrevented()) { 316 | return false; 317 | } 318 | var that = $(this).data('blueimp-fileupload') || 319 | $(this).data('fileupload'); 320 | that._resetFinishedDeferreds(); 321 | that._transition($(this).find('.fileupload-progress')).done( 322 | function () { 323 | that._trigger('started', e); 324 | } 325 | ); 326 | }, 327 | // Callback for uploads stop, equivalent to the global ajaxStop event: 328 | stop: function (e) { 329 | if (e.isDefaultPrevented()) { 330 | return false; 331 | } 332 | var that = $(this).data('blueimp-fileupload') || 333 | $(this).data('fileupload'), 334 | deferred = that._addFinishedDeferreds(); 335 | $.when.apply($, that._getFinishedDeferreds()) 336 | .done(function () { 337 | that._trigger('stopped', e); 338 | }); 339 | that._transition($(this).find('.fileupload-progress')).done( 340 | function () { 341 | $(this).find('.progress') 342 | .attr('aria-valuenow', '0') 343 | .children().first().css('width', '0%'); 344 | $(this).find('.progress-extended').html(' '); 345 | deferred.resolve(); 346 | } 347 | ); 348 | }, 349 | processstart: function (e) { 350 | if (e.isDefaultPrevented()) { 351 | return false; 352 | } 353 | $(this).addClass('fileupload-processing'); 354 | }, 355 | processstop: function (e) { 356 | if (e.isDefaultPrevented()) { 357 | return false; 358 | } 359 | $(this).removeClass('fileupload-processing'); 360 | }, 361 | // Callback for file deletion: 362 | destroy: function (e, data) { 363 | if (e.isDefaultPrevented()) { 364 | return false; 365 | } 366 | var that = $(this).data('blueimp-fileupload') || 367 | $(this).data('fileupload'), 368 | removeNode = function () { 369 | that._transition(data.context).done( 370 | function () { 371 | $(this).remove(); 372 | that._trigger('destroyed', e, data); 373 | } 374 | ); 375 | }; 376 | if (data.url) { 377 | data.dataType = data.dataType || that.options.dataType; 378 | $.ajax(data).done(removeNode).fail(function () { 379 | that._trigger('destroyfailed', e, data); 380 | }); 381 | } else { 382 | removeNode(); 383 | } 384 | } 385 | }, 386 | 387 | _resetFinishedDeferreds: function () { 388 | this._finishedUploads = []; 389 | }, 390 | 391 | _addFinishedDeferreds: function (deferred) { 392 | if (!deferred) { 393 | deferred = $.Deferred(); 394 | } 395 | this._finishedUploads.push(deferred); 396 | return deferred; 397 | }, 398 | 399 | _getFinishedDeferreds: function () { 400 | return this._finishedUploads; 401 | }, 402 | 403 | // Link handler, that allows to download files 404 | // by drag & drop of the links to the desktop: 405 | _enableDragToDesktop: function () { 406 | var link = $(this), 407 | url = link.prop('href'), 408 | name = link.prop('download'), 409 | type = 'application/octet-stream'; 410 | link.bind('dragstart', function (e) { 411 | try { 412 | e.originalEvent.dataTransfer.setData( 413 | 'DownloadURL', 414 | [type, name, url].join(':') 415 | ); 416 | } catch (ignore) {} 417 | }); 418 | }, 419 | 420 | _formatFileSize: function (bytes) { 421 | if (typeof bytes !== 'number') { 422 | return ''; 423 | } 424 | if (bytes >= 1000000000) { 425 | return (bytes / 1000000000).toFixed(2) + ' GB'; 426 | } 427 | if (bytes >= 1000000) { 428 | return (bytes / 1000000).toFixed(2) + ' MB'; 429 | } 430 | return (bytes / 1000).toFixed(2) + ' KB'; 431 | }, 432 | 433 | _formatBitrate: function (bits) { 434 | if (typeof bits !== 'number') { 435 | return ''; 436 | } 437 | if (bits >= 1000000000) { 438 | return (bits / 1000000000).toFixed(2) + ' Gbit/s'; 439 | } 440 | if (bits >= 1000000) { 441 | return (bits / 1000000).toFixed(2) + ' Mbit/s'; 442 | } 443 | if (bits >= 1000) { 444 | return (bits / 1000).toFixed(2) + ' kbit/s'; 445 | } 446 | return bits.toFixed(2) + ' bit/s'; 447 | }, 448 | 449 | _formatTime: function (seconds) { 450 | var date = new Date(seconds * 1000), 451 | days = Math.floor(seconds / 86400); 452 | days = days ? days + 'd ' : ''; 453 | return days + 454 | ('0' + date.getUTCHours()).slice(-2) + ':' + 455 | ('0' + date.getUTCMinutes()).slice(-2) + ':' + 456 | ('0' + date.getUTCSeconds()).slice(-2); 457 | }, 458 | 459 | _formatPercentage: function (floatValue) { 460 | return (floatValue * 100).toFixed(2) + ' %'; 461 | }, 462 | 463 | _renderExtendedProgress: function (data) { 464 | return this._formatBitrate(data.bitrate) + ' | ' + 465 | this._formatTime( 466 | (data.total - data.loaded) * 8 / data.bitrate 467 | ) + ' | ' + 468 | this._formatPercentage( 469 | data.loaded / data.total 470 | ) + ' | ' + 471 | this._formatFileSize(data.loaded) + ' / ' + 472 | this._formatFileSize(data.total); 473 | }, 474 | 475 | _renderTemplate: function (func, files) { 476 | if (!func) { 477 | return $(); 478 | } 479 | var result = func({ 480 | files: files, 481 | formatFileSize: this._formatFileSize, 482 | options: this.options 483 | }); 484 | if (result instanceof $) { 485 | return result; 486 | } 487 | return $(this.options.templatesContainer).html(result).children(); 488 | }, 489 | 490 | _renderPreviews: function (data) { 491 | data.context.find('.preview').each(function (index, elm) { 492 | $(elm).append(data.files[index].preview); 493 | }); 494 | }, 495 | 496 | _renderUpload: function (files) { 497 | return this._renderTemplate( 498 | this.options.uploadTemplate, 499 | files 500 | ); 501 | }, 502 | 503 | _renderDownload: function (files) { 504 | return this._renderTemplate( 505 | this.options.downloadTemplate, 506 | files 507 | ).find('a[download]').each(this._enableDragToDesktop).end(); 508 | }, 509 | 510 | _startHandler: function (e) { 511 | e.preventDefault(); 512 | var button = $(e.currentTarget), 513 | template = button.closest('.template-upload'), 514 | data = template.data('data'); 515 | button.prop('disabled', true); 516 | if (data && data.submit) { 517 | data.submit(); 518 | } 519 | }, 520 | 521 | _cancelHandler: function (e) { 522 | e.preventDefault(); 523 | var template = $(e.currentTarget) 524 | .closest('.template-upload,.template-download'), 525 | data = template.data('data') || {}; 526 | data.context = data.context || template; 527 | if (data.abort) { 528 | data.abort(); 529 | } else { 530 | data.errorThrown = 'abort'; 531 | this._trigger('fail', e, data); 532 | } 533 | }, 534 | 535 | _deleteHandler: function (e) { 536 | e.preventDefault(); 537 | var button = $(e.currentTarget); 538 | this._trigger('destroy', e, $.extend({ 539 | context: button.closest('.template-download'), 540 | type: 'DELETE' 541 | }, button.data())); 542 | }, 543 | 544 | _forceReflow: function (node) { 545 | return $.support.transition && node.length && 546 | node[0].offsetWidth; 547 | }, 548 | 549 | _transition: function (node) { 550 | var dfd = $.Deferred(); 551 | if ($.support.transition && node.hasClass('fade') && node.is(':visible')) { 552 | node.bind( 553 | $.support.transition.end, 554 | function (e) { 555 | // Make sure we don't respond to other transitions events 556 | // in the container element, e.g. from button elements: 557 | if (e.target === node[0]) { 558 | node.unbind($.support.transition.end); 559 | dfd.resolveWith(node); 560 | } 561 | } 562 | ).toggleClass('in'); 563 | } else { 564 | node.toggleClass('in'); 565 | dfd.resolveWith(node); 566 | } 567 | return dfd; 568 | }, 569 | 570 | _initButtonBarEventHandlers: function () { 571 | var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), 572 | filesList = this.options.filesContainer; 573 | this._on(fileUploadButtonBar.find('.start'), { 574 | click: function (e) { 575 | e.preventDefault(); 576 | filesList.find('.start').click(); 577 | } 578 | }); 579 | this._on(fileUploadButtonBar.find('.cancel'), { 580 | click: function (e) { 581 | e.preventDefault(); 582 | filesList.find('.cancel').click(); 583 | } 584 | }); 585 | this._on(fileUploadButtonBar.find('.delete'), { 586 | click: function (e) { 587 | e.preventDefault(); 588 | filesList.find('.toggle:checked') 589 | .closest('.template-download') 590 | .find('.delete').click(); 591 | fileUploadButtonBar.find('.toggle') 592 | .prop('checked', false); 593 | } 594 | }); 595 | this._on(fileUploadButtonBar.find('.toggle'), { 596 | change: function (e) { 597 | filesList.find('.toggle').prop( 598 | 'checked', 599 | $(e.currentTarget).is(':checked') 600 | ); 601 | } 602 | }); 603 | }, 604 | 605 | _destroyButtonBarEventHandlers: function () { 606 | this._off( 607 | this.element.find('.fileupload-buttonbar') 608 | .find('.start, .cancel, .delete'), 609 | 'click' 610 | ); 611 | this._off( 612 | this.element.find('.fileupload-buttonbar .toggle'), 613 | 'change.' 614 | ); 615 | }, 616 | 617 | _initEventHandlers: function () { 618 | this._super(); 619 | this._on(this.options.filesContainer, { 620 | 'click .start': this._startHandler, 621 | 'click .cancel': this._cancelHandler, 622 | 'click .delete': this._deleteHandler 623 | }); 624 | this._initButtonBarEventHandlers(); 625 | }, 626 | 627 | _destroyEventHandlers: function () { 628 | this._destroyButtonBarEventHandlers(); 629 | this._off(this.options.filesContainer, 'click'); 630 | this._super(); 631 | }, 632 | 633 | _enableFileInputButton: function () { 634 | this.element.find('.fileinput-button input') 635 | .prop('disabled', false) 636 | .parent().removeClass('disabled'); 637 | }, 638 | 639 | _disableFileInputButton: function () { 640 | this.element.find('.fileinput-button input') 641 | .prop('disabled', true) 642 | .parent().addClass('disabled'); 643 | }, 644 | 645 | _initTemplates: function () { 646 | var options = this.options; 647 | options.templatesContainer = this.document[0].createElement( 648 | options.filesContainer.prop('nodeName') 649 | ); 650 | if (tmpl) { 651 | if (options.uploadTemplateId) { 652 | options.uploadTemplate = tmpl(options.uploadTemplateId); 653 | } 654 | if (options.downloadTemplateId) { 655 | options.downloadTemplate = tmpl(options.downloadTemplateId); 656 | } 657 | } 658 | }, 659 | 660 | _initFilesContainer: function () { 661 | var options = this.options; 662 | if (options.filesContainer === undefined) { 663 | options.filesContainer = this.element.find('.files'); 664 | } else if (!(options.filesContainer instanceof $)) { 665 | options.filesContainer = $(options.filesContainer); 666 | } 667 | }, 668 | 669 | _initSpecialOptions: function () { 670 | this._super(); 671 | this._initFilesContainer(); 672 | this._initTemplates(); 673 | }, 674 | 675 | _create: function () { 676 | this._super(); 677 | this._resetFinishedDeferreds(); 678 | if (!$.support.fileInput) { 679 | this._disableFileInputButton(); 680 | } 681 | }, 682 | 683 | enable: function () { 684 | var wasDisabled = false; 685 | if (this.options.disabled) { 686 | wasDisabled = true; 687 | } 688 | this._super(); 689 | if (wasDisabled) { 690 | this.element.find('input, button').prop('disabled', false); 691 | this._enableFileInputButton(); 692 | } 693 | }, 694 | 695 | disable: function () { 696 | if (!this.options.disabled) { 697 | this.element.find('input, button').prop('disabled', true); 698 | this._disableFileInputButton(); 699 | } 700 | this._super(); 701 | } 702 | 703 | }); 704 | 705 | })); 706 | -------------------------------------------------------------------------------- /static/js/jquery.fileupload-validate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Validation Plugin 1.1.2 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, window */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define([ 19 | 'jquery', 20 | './jquery.fileupload-process' 21 | ], factory); 22 | } else { 23 | // Browser globals: 24 | factory( 25 | window.jQuery 26 | ); 27 | } 28 | }(function ($) { 29 | 'use strict'; 30 | 31 | // Append to the default processQueue: 32 | $.blueimp.fileupload.prototype.options.processQueue.push( 33 | { 34 | action: 'validate', 35 | // Always trigger this action, 36 | // even if the previous action was rejected: 37 | always: true, 38 | // Options taken from the global options map: 39 | acceptFileTypes: '@', 40 | maxFileSize: '@', 41 | minFileSize: '@', 42 | maxNumberOfFiles: '@', 43 | disabled: '@disableValidation' 44 | } 45 | ); 46 | 47 | // The File Upload Validation plugin extends the fileupload widget 48 | // with file validation functionality: 49 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 50 | 51 | options: { 52 | /* 53 | // The regular expression for allowed file types, matches 54 | // against either file type or file name: 55 | acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, 56 | // The maximum allowed file size in bytes: 57 | maxFileSize: 10000000, // 10 MB 58 | // The minimum allowed file size in bytes: 59 | minFileSize: undefined, // No minimal file size 60 | // The limit of files to be uploaded: 61 | maxNumberOfFiles: 10, 62 | */ 63 | 64 | // Function returning the current number of files, 65 | // has to be overriden for maxNumberOfFiles validation: 66 | getNumberOfFiles: $.noop, 67 | 68 | // Error and info messages: 69 | messages: { 70 | maxNumberOfFiles: 'Maximum number of files exceeded', 71 | acceptFileTypes: 'File type not allowed', 72 | maxFileSize: 'File is too large', 73 | minFileSize: 'File is too small' 74 | } 75 | }, 76 | 77 | processActions: { 78 | 79 | validate: function (data, options) { 80 | if (options.disabled) { 81 | return data; 82 | } 83 | var dfd = $.Deferred(), 84 | settings = this.options, 85 | file = data.files[data.index], 86 | fileSize; 87 | if (options.minFileSize || options.maxFileSize) { 88 | fileSize = file.size; 89 | } 90 | if ($.type(options.maxNumberOfFiles) === 'number' && 91 | (settings.getNumberOfFiles() || 0) + data.files.length > 92 | options.maxNumberOfFiles) { 93 | file.error = settings.i18n('maxNumberOfFiles'); 94 | } else if (options.acceptFileTypes && 95 | !(options.acceptFileTypes.test(file.type) || 96 | options.acceptFileTypes.test(file.name))) { 97 | file.error = settings.i18n('acceptFileTypes'); 98 | } else if (fileSize > options.maxFileSize) { 99 | file.error = settings.i18n('maxFileSize'); 100 | } else if ($.type(fileSize) === 'number' && 101 | fileSize < options.minFileSize) { 102 | file.error = settings.i18n('minFileSize'); 103 | } else { 104 | delete file.error; 105 | } 106 | if (file.error || data.files.error) { 107 | data.files.error = true; 108 | dfd.rejectWith(this, [data]); 109 | } else { 110 | dfd.resolveWith(this, [data]); 111 | } 112 | return dfd.promise(); 113 | } 114 | 115 | } 116 | 117 | }); 118 | 119 | })); 120 | -------------------------------------------------------------------------------- /static/js/jquery.fileupload-video.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Video Preview Plugin 1.0.3 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* jshint nomen:false */ 13 | /* global define, window, document */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define([ 20 | 'jquery', 21 | 'load-image', 22 | './jquery.fileupload-process' 23 | ], factory); 24 | } else { 25 | // Browser globals: 26 | factory( 27 | window.jQuery, 28 | window.loadImage 29 | ); 30 | } 31 | }(function ($, loadImage) { 32 | 'use strict'; 33 | 34 | // Prepend to the default processQueue: 35 | $.blueimp.fileupload.prototype.options.processQueue.unshift( 36 | { 37 | action: 'loadVideo', 38 | // Use the action as prefix for the "@" options: 39 | prefix: true, 40 | fileTypes: '@', 41 | maxFileSize: '@', 42 | disabled: '@disableVideoPreview' 43 | }, 44 | { 45 | action: 'setVideo', 46 | name: '@videoPreviewName', 47 | disabled: '@disableVideoPreview' 48 | } 49 | ); 50 | 51 | // The File Upload Video Preview plugin extends the fileupload widget 52 | // with video preview functionality: 53 | $.widget('blueimp.fileupload', $.blueimp.fileupload, { 54 | 55 | options: { 56 | // The regular expression for the types of video files to load, 57 | // matched against the file type: 58 | loadVideoFileTypes: /^video\/.*$/ 59 | }, 60 | 61 | _videoElement: document.createElement('video'), 62 | 63 | processActions: { 64 | 65 | // Loads the video file given via data.files and data.index 66 | // as video element if the browser supports playing it. 67 | // Accepts the options fileTypes (regular expression) 68 | // and maxFileSize (integer) to limit the files to load: 69 | loadVideo: function (data, options) { 70 | if (options.disabled) { 71 | return data; 72 | } 73 | var file = data.files[data.index], 74 | url, 75 | video; 76 | if (this._videoElement.canPlayType && 77 | this._videoElement.canPlayType(file.type) && 78 | ($.type(options.maxFileSize) !== 'number' || 79 | file.size <= options.maxFileSize) && 80 | (!options.fileTypes || 81 | options.fileTypes.test(file.type))) { 82 | url = loadImage.createObjectURL(file); 83 | if (url) { 84 | video = this._videoElement.cloneNode(false); 85 | video.src = url; 86 | video.controls = true; 87 | data.video = video; 88 | return data; 89 | } 90 | } 91 | return data; 92 | }, 93 | 94 | // Sets the video element as a property of the file object: 95 | setVideo: function (data, options) { 96 | if (data.video && !options.disabled) { 97 | data.files[data.index][options.name || 'preview'] = data.video; 98 | } 99 | return data; 100 | } 101 | 102 | } 103 | 104 | }); 105 | 106 | })); 107 | -------------------------------------------------------------------------------- /static/js/jquery.fileupload.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Plugin 5.41.0 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* jshint nomen:false */ 13 | /* global define, window, document, location, Blob, FormData */ 14 | 15 | (function (factory) { 16 | 'use strict'; 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define([ 20 | 'jquery', 21 | 'jquery.ui.widget' 22 | ], factory); 23 | } else { 24 | // Browser globals: 25 | factory(window.jQuery); 26 | } 27 | }(function ($) { 28 | 'use strict'; 29 | 30 | // Detect file input support, based on 31 | // http://viljamis.com/blog/2012/file-upload-support-on-mobile/ 32 | $.support.fileInput = !(new RegExp( 33 | // Handle devices which give false positives for the feature detection: 34 | '(Android (1\\.[0156]|2\\.[01]))' + 35 | '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + 36 | '|(w(eb)?OSBrowser)|(webOS)' + 37 | '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' 38 | ).test(window.navigator.userAgent) || 39 | // Feature detection for all other devices: 40 | $('').prop('disabled')); 41 | 42 | // The FileReader API is not actually used, but works as feature detection, 43 | // as some Safari versions (5?) support XHR file uploads via the FormData API, 44 | // but not non-multipart XHR file uploads. 45 | // window.XMLHttpRequestUpload is not available on IE10, so we check for 46 | // window.ProgressEvent instead to detect XHR2 file upload capability: 47 | $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); 48 | $.support.xhrFormDataFileUpload = !!window.FormData; 49 | 50 | // Detect support for Blob slicing (required for chunked uploads): 51 | $.support.blobSlice = window.Blob && (Blob.prototype.slice || 52 | Blob.prototype.webkitSlice || Blob.prototype.mozSlice); 53 | 54 | // The fileupload widget listens for change events on file input fields defined 55 | // via fileInput setting and paste or drop events of the given dropZone. 56 | // In addition to the default jQuery Widget methods, the fileupload widget 57 | // exposes the "add" and "send" methods, to add or directly send files using 58 | // the fileupload API. 59 | // By default, files added via file input selection, paste, drag & drop or 60 | // "add" method are uploaded immediately, but it is possible to override 61 | // the "add" callback option to queue file uploads. 62 | $.widget('blueimp.fileupload', { 63 | 64 | options: { 65 | // The drop target element(s), by the default the complete document. 66 | // Set to null to disable drag & drop support: 67 | dropZone: $(document), 68 | // The paste target element(s), by the default the complete document. 69 | // Set to null to disable paste support: 70 | pasteZone: $(document), 71 | // The file input field(s), that are listened to for change events. 72 | // If undefined, it is set to the file input fields inside 73 | // of the widget element on plugin initialization. 74 | // Set to null to disable the change listener. 75 | fileInput: undefined, 76 | // By default, the file input field is replaced with a clone after 77 | // each input field change event. This is required for iframe transport 78 | // queues and allows change events to be fired for the same file 79 | // selection, but can be disabled by setting the following option to false: 80 | replaceFileInput: true, 81 | // The parameter name for the file form data (the request argument name). 82 | // If undefined or empty, the name property of the file input field is 83 | // used, or "files[]" if the file input name property is also empty, 84 | // can be a string or an array of strings: 85 | paramName: undefined, 86 | // By default, each file of a selection is uploaded using an individual 87 | // request for XHR type uploads. Set to false to upload file 88 | // selections in one request each: 89 | singleFileUploads: true, 90 | // To limit the number of files uploaded with one XHR request, 91 | // set the following option to an integer greater than 0: 92 | limitMultiFileUploads: undefined, 93 | // The following option limits the number of files uploaded with one 94 | // XHR request to keep the request size under or equal to the defined 95 | // limit in bytes: 96 | limitMultiFileUploadSize: undefined, 97 | // Multipart file uploads add a number of bytes to each uploaded file, 98 | // therefore the following option adds an overhead for each file used 99 | // in the limitMultiFileUploadSize configuration: 100 | limitMultiFileUploadSizeOverhead: 512, 101 | // Set the following option to true to issue all file upload requests 102 | // in a sequential order: 103 | sequentialUploads: false, 104 | // To limit the number of concurrent uploads, 105 | // set the following option to an integer greater than 0: 106 | limitConcurrentUploads: undefined, 107 | // Set the following option to true to force iframe transport uploads: 108 | forceIframeTransport: false, 109 | // Set the following option to the location of a redirect url on the 110 | // origin server, for cross-domain iframe transport uploads: 111 | redirect: undefined, 112 | // The parameter name for the redirect url, sent as part of the form 113 | // data and set to 'redirect' if this option is empty: 114 | redirectParamName: undefined, 115 | // Set the following option to the location of a postMessage window, 116 | // to enable postMessage transport uploads: 117 | postMessage: undefined, 118 | // By default, XHR file uploads are sent as multipart/form-data. 119 | // The iframe transport is always using multipart/form-data. 120 | // Set to false to enable non-multipart XHR uploads: 121 | multipart: true, 122 | // To upload large files in smaller chunks, set the following option 123 | // to a preferred maximum chunk size. If set to 0, null or undefined, 124 | // or the browser does not support the required Blob API, files will 125 | // be uploaded as a whole. 126 | maxChunkSize: undefined, 127 | // When a non-multipart upload or a chunked multipart upload has been 128 | // aborted, this option can be used to resume the upload by setting 129 | // it to the size of the already uploaded bytes. This option is most 130 | // useful when modifying the options object inside of the "add" or 131 | // "send" callbacks, as the options are cloned for each file upload. 132 | uploadedBytes: undefined, 133 | // By default, failed (abort or error) file uploads are removed from the 134 | // global progress calculation. Set the following option to false to 135 | // prevent recalculating the global progress data: 136 | recalculateProgress: true, 137 | // Interval in milliseconds to calculate and trigger progress events: 138 | progressInterval: 100, 139 | // Interval in milliseconds to calculate progress bitrate: 140 | bitrateInterval: 500, 141 | // By default, uploads are started automatically when adding files: 142 | autoUpload: true, 143 | 144 | // Error and info messages: 145 | messages: { 146 | uploadedBytes: 'Uploaded bytes exceed file size' 147 | }, 148 | 149 | // Translation function, gets the message key to be translated 150 | // and an object with context specific data as arguments: 151 | i18n: function (message, context) { 152 | message = this.messages[message] || message.toString(); 153 | if (context) { 154 | $.each(context, function (key, value) { 155 | message = message.replace('{' + key + '}', value); 156 | }); 157 | } 158 | return message; 159 | }, 160 | 161 | // Additional form data to be sent along with the file uploads can be set 162 | // using this option, which accepts an array of objects with name and 163 | // value properties, a function returning such an array, a FormData 164 | // object (for XHR file uploads), or a simple object. 165 | // The form of the first fileInput is given as parameter to the function: 166 | formData: function (form) { 167 | return form.serializeArray(); 168 | }, 169 | 170 | // The add callback is invoked as soon as files are added to the fileupload 171 | // widget (via file input selection, drag & drop, paste or add API call). 172 | // If the singleFileUploads option is enabled, this callback will be 173 | // called once for each file in the selection for XHR file uploads, else 174 | // once for each file selection. 175 | // 176 | // The upload starts when the submit method is invoked on the data parameter. 177 | // The data object contains a files property holding the added files 178 | // and allows you to override plugin options as well as define ajax settings. 179 | // 180 | // Listeners for this callback can also be bound the following way: 181 | // .bind('fileuploadadd', func); 182 | // 183 | // data.submit() returns a Promise object and allows to attach additional 184 | // handlers using jQuery's Deferred callbacks: 185 | // data.submit().done(func).fail(func).always(func); 186 | add: function (e, data) { 187 | if (e.isDefaultPrevented()) { 188 | return false; 189 | } 190 | if (data.autoUpload || (data.autoUpload !== false && 191 | $(this).fileupload('option', 'autoUpload'))) { 192 | data.process().done(function () { 193 | data.submit(); 194 | }); 195 | } 196 | }, 197 | 198 | // Other callbacks: 199 | 200 | // Callback for the submit event of each file upload: 201 | // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); 202 | 203 | // Callback for the start of each file upload request: 204 | // send: function (e, data) {}, // .bind('fileuploadsend', func); 205 | 206 | // Callback for successful uploads: 207 | // done: function (e, data) {}, // .bind('fileuploaddone', func); 208 | 209 | // Callback for failed (abort or error) uploads: 210 | // fail: function (e, data) {}, // .bind('fileuploadfail', func); 211 | 212 | // Callback for completed (success, abort or error) requests: 213 | // always: function (e, data) {}, // .bind('fileuploadalways', func); 214 | 215 | // Callback for upload progress events: 216 | // progress: function (e, data) {}, // .bind('fileuploadprogress', func); 217 | 218 | // Callback for global upload progress events: 219 | // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); 220 | 221 | // Callback for uploads start, equivalent to the global ajaxStart event: 222 | // start: function (e) {}, // .bind('fileuploadstart', func); 223 | 224 | // Callback for uploads stop, equivalent to the global ajaxStop event: 225 | // stop: function (e) {}, // .bind('fileuploadstop', func); 226 | 227 | // Callback for change events of the fileInput(s): 228 | // change: function (e, data) {}, // .bind('fileuploadchange', func); 229 | 230 | // Callback for paste events to the pasteZone(s): 231 | // paste: function (e, data) {}, // .bind('fileuploadpaste', func); 232 | 233 | // Callback for drop events of the dropZone(s): 234 | // drop: function (e, data) {}, // .bind('fileuploaddrop', func); 235 | 236 | // Callback for dragover events of the dropZone(s): 237 | // dragover: function (e) {}, // .bind('fileuploaddragover', func); 238 | 239 | // Callback for the start of each chunk upload request: 240 | // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); 241 | 242 | // Callback for successful chunk uploads: 243 | // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); 244 | 245 | // Callback for failed (abort or error) chunk uploads: 246 | // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); 247 | 248 | // Callback for completed (success, abort or error) chunk upload requests: 249 | // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); 250 | 251 | // The plugin options are used as settings object for the ajax calls. 252 | // The following are jQuery ajax settings required for the file uploads: 253 | processData: false, 254 | contentType: false, 255 | cache: false 256 | }, 257 | 258 | // A list of options that require reinitializing event listeners and/or 259 | // special initialization code: 260 | _specialOptions: [ 261 | 'fileInput', 262 | 'dropZone', 263 | 'pasteZone', 264 | 'multipart', 265 | 'forceIframeTransport' 266 | ], 267 | 268 | _blobSlice: $.support.blobSlice && function () { 269 | var slice = this.slice || this.webkitSlice || this.mozSlice; 270 | return slice.apply(this, arguments); 271 | }, 272 | 273 | _BitrateTimer: function () { 274 | this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); 275 | this.loaded = 0; 276 | this.bitrate = 0; 277 | this.getBitrate = function (now, loaded, interval) { 278 | var timeDiff = now - this.timestamp; 279 | if (!this.bitrate || !interval || timeDiff > interval) { 280 | this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; 281 | this.loaded = loaded; 282 | this.timestamp = now; 283 | } 284 | return this.bitrate; 285 | }; 286 | }, 287 | 288 | _isXHRUpload: function (options) { 289 | return !options.forceIframeTransport && 290 | ((!options.multipart && $.support.xhrFileUpload) || 291 | $.support.xhrFormDataFileUpload); 292 | }, 293 | 294 | _getFormData: function (options) { 295 | var formData; 296 | if ($.type(options.formData) === 'function') { 297 | return options.formData(options.form); 298 | } 299 | if ($.isArray(options.formData)) { 300 | return options.formData; 301 | } 302 | if ($.type(options.formData) === 'object') { 303 | formData = []; 304 | $.each(options.formData, function (name, value) { 305 | formData.push({name: name, value: value}); 306 | }); 307 | return formData; 308 | } 309 | return []; 310 | }, 311 | 312 | _getTotal: function (files) { 313 | var total = 0; 314 | $.each(files, function (index, file) { 315 | total += file.size || 1; 316 | }); 317 | return total; 318 | }, 319 | 320 | _initProgressObject: function (obj) { 321 | var progress = { 322 | loaded: 0, 323 | total: 0, 324 | bitrate: 0 325 | }; 326 | if (obj._progress) { 327 | $.extend(obj._progress, progress); 328 | } else { 329 | obj._progress = progress; 330 | } 331 | }, 332 | 333 | _initResponseObject: function (obj) { 334 | var prop; 335 | if (obj._response) { 336 | for (prop in obj._response) { 337 | if (obj._response.hasOwnProperty(prop)) { 338 | delete obj._response[prop]; 339 | } 340 | } 341 | } else { 342 | obj._response = {}; 343 | } 344 | }, 345 | 346 | _onProgress: function (e, data) { 347 | if (e.lengthComputable) { 348 | var now = ((Date.now) ? Date.now() : (new Date()).getTime()), 349 | loaded; 350 | if (data._time && data.progressInterval && 351 | (now - data._time < data.progressInterval) && 352 | e.loaded !== e.total) { 353 | return; 354 | } 355 | data._time = now; 356 | loaded = Math.floor( 357 | e.loaded / e.total * (data.chunkSize || data._progress.total) 358 | ) + (data.uploadedBytes || 0); 359 | // Add the difference from the previously loaded state 360 | // to the global loaded counter: 361 | this._progress.loaded += (loaded - data._progress.loaded); 362 | this._progress.bitrate = this._bitrateTimer.getBitrate( 363 | now, 364 | this._progress.loaded, 365 | data.bitrateInterval 366 | ); 367 | data._progress.loaded = data.loaded = loaded; 368 | data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( 369 | now, 370 | loaded, 371 | data.bitrateInterval 372 | ); 373 | // Trigger a custom progress event with a total data property set 374 | // to the file size(s) of the current upload and a loaded data 375 | // property calculated accordingly: 376 | this._trigger( 377 | 'progress', 378 | $.Event('progress', {delegatedEvent: e}), 379 | data 380 | ); 381 | // Trigger a global progress event for all current file uploads, 382 | // including ajax calls queued for sequential file uploads: 383 | this._trigger( 384 | 'progressall', 385 | $.Event('progressall', {delegatedEvent: e}), 386 | this._progress 387 | ); 388 | } 389 | }, 390 | 391 | _initProgressListener: function (options) { 392 | var that = this, 393 | xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); 394 | // Accesss to the native XHR object is required to add event listeners 395 | // for the upload progress event: 396 | if (xhr.upload) { 397 | $(xhr.upload).bind('progress', function (e) { 398 | var oe = e.originalEvent; 399 | // Make sure the progress event properties get copied over: 400 | e.lengthComputable = oe.lengthComputable; 401 | e.loaded = oe.loaded; 402 | e.total = oe.total; 403 | that._onProgress(e, options); 404 | }); 405 | options.xhr = function () { 406 | return xhr; 407 | }; 408 | } 409 | }, 410 | 411 | _isInstanceOf: function (type, obj) { 412 | // Cross-frame instanceof check 413 | return Object.prototype.toString.call(obj) === '[object ' + type + ']'; 414 | }, 415 | 416 | _initXHRData: function (options) { 417 | var that = this, 418 | formData, 419 | file = options.files[0], 420 | // Ignore non-multipart setting if not supported: 421 | multipart = options.multipart || !$.support.xhrFileUpload, 422 | paramName = $.type(options.paramName) === 'array' ? 423 | options.paramName[0] : options.paramName; 424 | options.headers = $.extend({}, options.headers); 425 | if (options.contentRange) { 426 | options.headers['Content-Range'] = options.contentRange; 427 | } 428 | if (!multipart || options.blob || !this._isInstanceOf('File', file)) { 429 | options.headers['Content-Disposition'] = 'attachment; filename="' + 430 | encodeURI(file.name) + '"'; 431 | } 432 | if (!multipart) { 433 | options.contentType = file.type || 'application/octet-stream'; 434 | options.data = options.blob || file; 435 | } else if ($.support.xhrFormDataFileUpload) { 436 | if (options.postMessage) { 437 | // window.postMessage does not allow sending FormData 438 | // objects, so we just add the File/Blob objects to 439 | // the formData array and let the postMessage window 440 | // create the FormData object out of this array: 441 | formData = this._getFormData(options); 442 | if (options.blob) { 443 | formData.push({ 444 | name: paramName, 445 | value: options.blob 446 | }); 447 | } else { 448 | $.each(options.files, function (index, file) { 449 | formData.push({ 450 | name: ($.type(options.paramName) === 'array' && 451 | options.paramName[index]) || paramName, 452 | value: file 453 | }); 454 | }); 455 | } 456 | } else { 457 | if (that._isInstanceOf('FormData', options.formData)) { 458 | formData = options.formData; 459 | } else { 460 | formData = new FormData(); 461 | $.each(this._getFormData(options), function (index, field) { 462 | formData.append(field.name, field.value); 463 | }); 464 | } 465 | if (options.blob) { 466 | formData.append(paramName, options.blob, file.name); 467 | } else { 468 | $.each(options.files, function (index, file) { 469 | // This check allows the tests to run with 470 | // dummy objects: 471 | if (that._isInstanceOf('File', file) || 472 | that._isInstanceOf('Blob', file)) { 473 | formData.append( 474 | ($.type(options.paramName) === 'array' && 475 | options.paramName[index]) || paramName, 476 | file, 477 | file.uploadName || file.name 478 | ); 479 | } 480 | }); 481 | } 482 | } 483 | options.data = formData; 484 | } 485 | // Blob reference is not needed anymore, free memory: 486 | options.blob = null; 487 | }, 488 | 489 | _initIframeSettings: function (options) { 490 | var targetHost = $('').prop('href', options.url).prop('host'); 491 | // Setting the dataType to iframe enables the iframe transport: 492 | options.dataType = 'iframe ' + (options.dataType || ''); 493 | // The iframe transport accepts a serialized array as form data: 494 | options.formData = this._getFormData(options); 495 | // Add redirect url to form data on cross-domain uploads: 496 | if (options.redirect && targetHost && targetHost !== location.host) { 497 | options.formData.push({ 498 | name: options.redirectParamName || 'redirect', 499 | value: options.redirect 500 | }); 501 | } 502 | }, 503 | 504 | _initDataSettings: function (options) { 505 | if (this._isXHRUpload(options)) { 506 | if (!this._chunkedUpload(options, true)) { 507 | if (!options.data) { 508 | this._initXHRData(options); 509 | } 510 | this._initProgressListener(options); 511 | } 512 | if (options.postMessage) { 513 | // Setting the dataType to postmessage enables the 514 | // postMessage transport: 515 | options.dataType = 'postmessage ' + (options.dataType || ''); 516 | } 517 | } else { 518 | this._initIframeSettings(options); 519 | } 520 | }, 521 | 522 | _getParamName: function (options) { 523 | var fileInput = $(options.fileInput), 524 | paramName = options.paramName; 525 | if (!paramName) { 526 | paramName = []; 527 | fileInput.each(function () { 528 | var input = $(this), 529 | name = input.prop('name') || 'files[]', 530 | i = (input.prop('files') || [1]).length; 531 | while (i) { 532 | paramName.push(name); 533 | i -= 1; 534 | } 535 | }); 536 | if (!paramName.length) { 537 | paramName = [fileInput.prop('name') || 'files[]']; 538 | } 539 | } else if (!$.isArray(paramName)) { 540 | paramName = [paramName]; 541 | } 542 | return paramName; 543 | }, 544 | 545 | _initFormSettings: function (options) { 546 | // Retrieve missing options from the input field and the 547 | // associated form, if available: 548 | if (!options.form || !options.form.length) { 549 | options.form = $(options.fileInput.prop('form')); 550 | // If the given file input doesn't have an associated form, 551 | // use the default widget file input's form: 552 | if (!options.form.length) { 553 | options.form = $(this.options.fileInput.prop('form')); 554 | } 555 | } 556 | options.paramName = this._getParamName(options); 557 | if (!options.url) { 558 | options.url = options.form.prop('action') || location.href; 559 | } 560 | // The HTTP request method must be "POST" or "PUT": 561 | options.type = (options.type || 562 | ($.type(options.form.prop('method')) === 'string' && 563 | options.form.prop('method')) || '' 564 | ).toUpperCase(); 565 | if (options.type !== 'POST' && options.type !== 'PUT' && 566 | options.type !== 'PATCH') { 567 | options.type = 'POST'; 568 | } 569 | if (!options.formAcceptCharset) { 570 | options.formAcceptCharset = options.form.attr('accept-charset'); 571 | } 572 | }, 573 | 574 | _getAJAXSettings: function (data) { 575 | var options = $.extend({}, this.options, data); 576 | this._initFormSettings(options); 577 | this._initDataSettings(options); 578 | return options; 579 | }, 580 | 581 | // jQuery 1.6 doesn't provide .state(), 582 | // while jQuery 1.8+ removed .isRejected() and .isResolved(): 583 | _getDeferredState: function (deferred) { 584 | if (deferred.state) { 585 | return deferred.state(); 586 | } 587 | if (deferred.isResolved()) { 588 | return 'resolved'; 589 | } 590 | if (deferred.isRejected()) { 591 | return 'rejected'; 592 | } 593 | return 'pending'; 594 | }, 595 | 596 | // Maps jqXHR callbacks to the equivalent 597 | // methods of the given Promise object: 598 | _enhancePromise: function (promise) { 599 | promise.success = promise.done; 600 | promise.error = promise.fail; 601 | promise.complete = promise.always; 602 | return promise; 603 | }, 604 | 605 | // Creates and returns a Promise object enhanced with 606 | // the jqXHR methods abort, success, error and complete: 607 | _getXHRPromise: function (resolveOrReject, context, args) { 608 | var dfd = $.Deferred(), 609 | promise = dfd.promise(); 610 | context = context || this.options.context || promise; 611 | if (resolveOrReject === true) { 612 | dfd.resolveWith(context, args); 613 | } else if (resolveOrReject === false) { 614 | dfd.rejectWith(context, args); 615 | } 616 | promise.abort = dfd.promise; 617 | return this._enhancePromise(promise); 618 | }, 619 | 620 | // Adds convenience methods to the data callback argument: 621 | _addConvenienceMethods: function (e, data) { 622 | var that = this, 623 | getPromise = function (args) { 624 | return $.Deferred().resolveWith(that, args).promise(); 625 | }; 626 | data.process = function (resolveFunc, rejectFunc) { 627 | if (resolveFunc || rejectFunc) { 628 | data._processQueue = this._processQueue = 629 | (this._processQueue || getPromise([this])).pipe( 630 | function () { 631 | if (data.errorThrown) { 632 | return $.Deferred() 633 | .rejectWith(that, [data]).promise(); 634 | } 635 | return getPromise(arguments); 636 | } 637 | ).pipe(resolveFunc, rejectFunc); 638 | } 639 | return this._processQueue || getPromise([this]); 640 | }; 641 | data.submit = function () { 642 | if (this.state() !== 'pending') { 643 | data.jqXHR = this.jqXHR = 644 | (that._trigger( 645 | 'submit', 646 | $.Event('submit', {delegatedEvent: e}), 647 | this 648 | ) !== false) && that._onSend(e, this); 649 | } 650 | return this.jqXHR || that._getXHRPromise(); 651 | }; 652 | data.abort = function () { 653 | if (this.jqXHR) { 654 | return this.jqXHR.abort(); 655 | } 656 | this.errorThrown = 'abort'; 657 | that._trigger('fail', null, this); 658 | return that._getXHRPromise(false); 659 | }; 660 | data.state = function () { 661 | if (this.jqXHR) { 662 | return that._getDeferredState(this.jqXHR); 663 | } 664 | if (this._processQueue) { 665 | return that._getDeferredState(this._processQueue); 666 | } 667 | }; 668 | data.processing = function () { 669 | return !this.jqXHR && this._processQueue && that 670 | ._getDeferredState(this._processQueue) === 'pending'; 671 | }; 672 | data.progress = function () { 673 | return this._progress; 674 | }; 675 | data.response = function () { 676 | return this._response; 677 | }; 678 | }, 679 | 680 | // Parses the Range header from the server response 681 | // and returns the uploaded bytes: 682 | _getUploadedBytes: function (jqXHR) { 683 | var range = jqXHR.getResponseHeader('Range'), 684 | parts = range && range.split('-'), 685 | upperBytesPos = parts && parts.length > 1 && 686 | parseInt(parts[1], 10); 687 | return upperBytesPos && upperBytesPos + 1; 688 | }, 689 | 690 | // Uploads a file in multiple, sequential requests 691 | // by splitting the file up in multiple blob chunks. 692 | // If the second parameter is true, only tests if the file 693 | // should be uploaded in chunks, but does not invoke any 694 | // upload requests: 695 | _chunkedUpload: function (options, testOnly) { 696 | options.uploadedBytes = options.uploadedBytes || 0; 697 | var that = this, 698 | file = options.files[0], 699 | fs = file.size, 700 | ub = options.uploadedBytes, 701 | mcs = options.maxChunkSize || fs, 702 | slice = this._blobSlice, 703 | dfd = $.Deferred(), 704 | promise = dfd.promise(), 705 | jqXHR, 706 | upload; 707 | if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || 708 | options.data) { 709 | return false; 710 | } 711 | if (testOnly) { 712 | return true; 713 | } 714 | if (ub >= fs) { 715 | file.error = options.i18n('uploadedBytes'); 716 | return this._getXHRPromise( 717 | false, 718 | options.context, 719 | [null, 'error', file.error] 720 | ); 721 | } 722 | // The chunk upload method: 723 | upload = function () { 724 | // Clone the options object for each chunk upload: 725 | var o = $.extend({}, options), 726 | currentLoaded = o._progress.loaded; 727 | o.blob = slice.call( 728 | file, 729 | ub, 730 | ub + mcs, 731 | file.type 732 | ); 733 | // Store the current chunk size, as the blob itself 734 | // will be dereferenced after data processing: 735 | o.chunkSize = o.blob.size; 736 | // Expose the chunk bytes position range: 737 | o.contentRange = 'bytes ' + ub + '-' + 738 | (ub + o.chunkSize - 1) + '/' + fs; 739 | // Process the upload data (the blob and potential form data): 740 | that._initXHRData(o); 741 | // Add progress listeners for this chunk upload: 742 | that._initProgressListener(o); 743 | jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || 744 | that._getXHRPromise(false, o.context)) 745 | .done(function (result, textStatus, jqXHR) { 746 | ub = that._getUploadedBytes(jqXHR) || 747 | (ub + o.chunkSize); 748 | // Create a progress event if no final progress event 749 | // with loaded equaling total has been triggered 750 | // for this chunk: 751 | if (currentLoaded + o.chunkSize - o._progress.loaded) { 752 | that._onProgress($.Event('progress', { 753 | lengthComputable: true, 754 | loaded: ub - o.uploadedBytes, 755 | total: ub - o.uploadedBytes 756 | }), o); 757 | } 758 | options.uploadedBytes = o.uploadedBytes = ub; 759 | o.result = result; 760 | o.textStatus = textStatus; 761 | o.jqXHR = jqXHR; 762 | that._trigger('chunkdone', null, o); 763 | that._trigger('chunkalways', null, o); 764 | if (ub < fs) { 765 | // File upload not yet complete, 766 | // continue with the next chunk: 767 | upload(); 768 | } else { 769 | dfd.resolveWith( 770 | o.context, 771 | [result, textStatus, jqXHR] 772 | ); 773 | } 774 | }) 775 | .fail(function (jqXHR, textStatus, errorThrown) { 776 | o.jqXHR = jqXHR; 777 | o.textStatus = textStatus; 778 | o.errorThrown = errorThrown; 779 | that._trigger('chunkfail', null, o); 780 | that._trigger('chunkalways', null, o); 781 | dfd.rejectWith( 782 | o.context, 783 | [jqXHR, textStatus, errorThrown] 784 | ); 785 | }); 786 | }; 787 | this._enhancePromise(promise); 788 | promise.abort = function () { 789 | return jqXHR.abort(); 790 | }; 791 | upload(); 792 | return promise; 793 | }, 794 | 795 | _beforeSend: function (e, data) { 796 | if (this._active === 0) { 797 | // the start callback is triggered when an upload starts 798 | // and no other uploads are currently running, 799 | // equivalent to the global ajaxStart event: 800 | this._trigger('start'); 801 | // Set timer for global bitrate progress calculation: 802 | this._bitrateTimer = new this._BitrateTimer(); 803 | // Reset the global progress values: 804 | this._progress.loaded = this._progress.total = 0; 805 | this._progress.bitrate = 0; 806 | } 807 | // Make sure the container objects for the .response() and 808 | // .progress() methods on the data object are available 809 | // and reset to their initial state: 810 | this._initResponseObject(data); 811 | this._initProgressObject(data); 812 | data._progress.loaded = data.loaded = data.uploadedBytes || 0; 813 | data._progress.total = data.total = this._getTotal(data.files) || 1; 814 | data._progress.bitrate = data.bitrate = 0; 815 | this._active += 1; 816 | // Initialize the global progress values: 817 | this._progress.loaded += data.loaded; 818 | this._progress.total += data.total; 819 | }, 820 | 821 | _onDone: function (result, textStatus, jqXHR, options) { 822 | var total = options._progress.total, 823 | response = options._response; 824 | if (options._progress.loaded < total) { 825 | // Create a progress event if no final progress event 826 | // with loaded equaling total has been triggered: 827 | this._onProgress($.Event('progress', { 828 | lengthComputable: true, 829 | loaded: total, 830 | total: total 831 | }), options); 832 | } 833 | response.result = options.result = result; 834 | response.textStatus = options.textStatus = textStatus; 835 | response.jqXHR = options.jqXHR = jqXHR; 836 | this._trigger('done', null, options); 837 | }, 838 | 839 | _onFail: function (jqXHR, textStatus, errorThrown, options) { 840 | var response = options._response; 841 | if (options.recalculateProgress) { 842 | // Remove the failed (error or abort) file upload from 843 | // the global progress calculation: 844 | this._progress.loaded -= options._progress.loaded; 845 | this._progress.total -= options._progress.total; 846 | } 847 | response.jqXHR = options.jqXHR = jqXHR; 848 | response.textStatus = options.textStatus = textStatus; 849 | response.errorThrown = options.errorThrown = errorThrown; 850 | this._trigger('fail', null, options); 851 | }, 852 | 853 | _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { 854 | // jqXHRorResult, textStatus and jqXHRorError are added to the 855 | // options object via done and fail callbacks 856 | this._trigger('always', null, options); 857 | }, 858 | 859 | _onSend: function (e, data) { 860 | if (!data.submit) { 861 | this._addConvenienceMethods(e, data); 862 | } 863 | var that = this, 864 | jqXHR, 865 | aborted, 866 | slot, 867 | pipe, 868 | options = that._getAJAXSettings(data), 869 | send = function () { 870 | that._sending += 1; 871 | // Set timer for bitrate progress calculation: 872 | options._bitrateTimer = new that._BitrateTimer(); 873 | jqXHR = jqXHR || ( 874 | ((aborted || that._trigger( 875 | 'send', 876 | $.Event('send', {delegatedEvent: e}), 877 | options 878 | ) === false) && 879 | that._getXHRPromise(false, options.context, aborted)) || 880 | that._chunkedUpload(options) || $.ajax(options) 881 | ).done(function (result, textStatus, jqXHR) { 882 | that._onDone(result, textStatus, jqXHR, options); 883 | }).fail(function (jqXHR, textStatus, errorThrown) { 884 | that._onFail(jqXHR, textStatus, errorThrown, options); 885 | }).always(function (jqXHRorResult, textStatus, jqXHRorError) { 886 | that._onAlways( 887 | jqXHRorResult, 888 | textStatus, 889 | jqXHRorError, 890 | options 891 | ); 892 | that._sending -= 1; 893 | that._active -= 1; 894 | if (options.limitConcurrentUploads && 895 | options.limitConcurrentUploads > that._sending) { 896 | // Start the next queued upload, 897 | // that has not been aborted: 898 | var nextSlot = that._slots.shift(); 899 | while (nextSlot) { 900 | if (that._getDeferredState(nextSlot) === 'pending') { 901 | nextSlot.resolve(); 902 | break; 903 | } 904 | nextSlot = that._slots.shift(); 905 | } 906 | } 907 | if (that._active === 0) { 908 | // The stop callback is triggered when all uploads have 909 | // been completed, equivalent to the global ajaxStop event: 910 | that._trigger('stop'); 911 | } 912 | }); 913 | return jqXHR; 914 | }; 915 | this._beforeSend(e, options); 916 | if (this.options.sequentialUploads || 917 | (this.options.limitConcurrentUploads && 918 | this.options.limitConcurrentUploads <= this._sending)) { 919 | if (this.options.limitConcurrentUploads > 1) { 920 | slot = $.Deferred(); 921 | this._slots.push(slot); 922 | pipe = slot.pipe(send); 923 | } else { 924 | this._sequence = this._sequence.pipe(send, send); 925 | pipe = this._sequence; 926 | } 927 | // Return the piped Promise object, enhanced with an abort method, 928 | // which is delegated to the jqXHR object of the current upload, 929 | // and jqXHR callbacks mapped to the equivalent Promise methods: 930 | pipe.abort = function () { 931 | aborted = [undefined, 'abort', 'abort']; 932 | if (!jqXHR) { 933 | if (slot) { 934 | slot.rejectWith(options.context, aborted); 935 | } 936 | return send(); 937 | } 938 | return jqXHR.abort(); 939 | }; 940 | return this._enhancePromise(pipe); 941 | } 942 | return send(); 943 | }, 944 | 945 | _onAdd: function (e, data) { 946 | var that = this, 947 | result = true, 948 | options = $.extend({}, this.options, data), 949 | files = data.files, 950 | filesLength = files.length, 951 | limit = options.limitMultiFileUploads, 952 | limitSize = options.limitMultiFileUploadSize, 953 | overhead = options.limitMultiFileUploadSizeOverhead, 954 | batchSize = 0, 955 | paramName = this._getParamName(options), 956 | paramNameSet, 957 | paramNameSlice, 958 | fileSet, 959 | i, 960 | j = 0; 961 | if (limitSize && (!filesLength || files[0].size === undefined)) { 962 | limitSize = undefined; 963 | } 964 | if (!(options.singleFileUploads || limit || limitSize) || 965 | !this._isXHRUpload(options)) { 966 | fileSet = [files]; 967 | paramNameSet = [paramName]; 968 | } else if (!(options.singleFileUploads || limitSize) && limit) { 969 | fileSet = []; 970 | paramNameSet = []; 971 | for (i = 0; i < filesLength; i += limit) { 972 | fileSet.push(files.slice(i, i + limit)); 973 | paramNameSlice = paramName.slice(i, i + limit); 974 | if (!paramNameSlice.length) { 975 | paramNameSlice = paramName; 976 | } 977 | paramNameSet.push(paramNameSlice); 978 | } 979 | } else if (!options.singleFileUploads && limitSize) { 980 | fileSet = []; 981 | paramNameSet = []; 982 | for (i = 0; i < filesLength; i = i + 1) { 983 | batchSize += files[i].size + overhead; 984 | if (i + 1 === filesLength || 985 | ((batchSize + files[i + 1].size + overhead) > limitSize) || 986 | (limit && i + 1 - j >= limit)) { 987 | fileSet.push(files.slice(j, i + 1)); 988 | paramNameSlice = paramName.slice(j, i + 1); 989 | if (!paramNameSlice.length) { 990 | paramNameSlice = paramName; 991 | } 992 | paramNameSet.push(paramNameSlice); 993 | j = i + 1; 994 | batchSize = 0; 995 | } 996 | } 997 | } else { 998 | paramNameSet = paramName; 999 | } 1000 | data.originalFiles = files; 1001 | $.each(fileSet || files, function (index, element) { 1002 | var newData = $.extend({}, data); 1003 | newData.files = fileSet ? element : [element]; 1004 | newData.paramName = paramNameSet[index]; 1005 | that._initResponseObject(newData); 1006 | that._initProgressObject(newData); 1007 | that._addConvenienceMethods(e, newData); 1008 | result = that._trigger( 1009 | 'add', 1010 | $.Event('add', {delegatedEvent: e}), 1011 | newData 1012 | ); 1013 | return result; 1014 | }); 1015 | return result; 1016 | }, 1017 | 1018 | _replaceFileInput: function (data) { 1019 | var input = data.fileInput, 1020 | inputClone = input.clone(true); 1021 | // Add a reference for the new cloned file input to the data argument: 1022 | data.fileInputClone = inputClone; 1023 | $('
').append(inputClone)[0].reset(); 1024 | // Detaching allows to insert the fileInput on another form 1025 | // without loosing the file input value: 1026 | input.after(inputClone).detach(); 1027 | // Avoid memory leaks with the detached file input: 1028 | $.cleanData(input.unbind('remove')); 1029 | // Replace the original file input element in the fileInput 1030 | // elements set with the clone, which has been copied including 1031 | // event handlers: 1032 | this.options.fileInput = this.options.fileInput.map(function (i, el) { 1033 | if (el === input[0]) { 1034 | return inputClone[0]; 1035 | } 1036 | return el; 1037 | }); 1038 | // If the widget has been initialized on the file input itself, 1039 | // override this.element with the file input clone: 1040 | if (input[0] === this.element[0]) { 1041 | this.element = inputClone; 1042 | } 1043 | }, 1044 | 1045 | _handleFileTreeEntry: function (entry, path) { 1046 | var that = this, 1047 | dfd = $.Deferred(), 1048 | errorHandler = function (e) { 1049 | if (e && !e.entry) { 1050 | e.entry = entry; 1051 | } 1052 | // Since $.when returns immediately if one 1053 | // Deferred is rejected, we use resolve instead. 1054 | // This allows valid files and invalid items 1055 | // to be returned together in one set: 1056 | dfd.resolve([e]); 1057 | }, 1058 | successHandler = function (entries) { 1059 | that._handleFileTreeEntries( 1060 | entries, 1061 | path + entry.name + '/' 1062 | ).done(function (files) { 1063 | dfd.resolve(files); 1064 | }).fail(errorHandler); 1065 | }, 1066 | readEntries = function () { 1067 | dirReader.readEntries(function (results) { 1068 | if (!results.length) { 1069 | successHandler(entries); 1070 | } else { 1071 | entries = entries.concat(results); 1072 | readEntries(); 1073 | } 1074 | }, errorHandler); 1075 | }, 1076 | dirReader, entries = []; 1077 | path = path || ''; 1078 | if (entry.isFile) { 1079 | if (entry._file) { 1080 | // Workaround for Chrome bug #149735 1081 | entry._file.relativePath = path; 1082 | dfd.resolve(entry._file); 1083 | } else { 1084 | entry.file(function (file) { 1085 | file.relativePath = path; 1086 | dfd.resolve(file); 1087 | }, errorHandler); 1088 | } 1089 | } else if (entry.isDirectory) { 1090 | dirReader = entry.createReader(); 1091 | readEntries(); 1092 | } else { 1093 | // Return an empy list for file system items 1094 | // other than files or directories: 1095 | dfd.resolve([]); 1096 | } 1097 | return dfd.promise(); 1098 | }, 1099 | 1100 | _handleFileTreeEntries: function (entries, path) { 1101 | var that = this; 1102 | return $.when.apply( 1103 | $, 1104 | $.map(entries, function (entry) { 1105 | return that._handleFileTreeEntry(entry, path); 1106 | }) 1107 | ).pipe(function () { 1108 | return Array.prototype.concat.apply( 1109 | [], 1110 | arguments 1111 | ); 1112 | }); 1113 | }, 1114 | 1115 | _getDroppedFiles: function (dataTransfer) { 1116 | dataTransfer = dataTransfer || {}; 1117 | var items = dataTransfer.items; 1118 | if (items && items.length && (items[0].webkitGetAsEntry || 1119 | items[0].getAsEntry)) { 1120 | return this._handleFileTreeEntries( 1121 | $.map(items, function (item) { 1122 | var entry; 1123 | if (item.webkitGetAsEntry) { 1124 | entry = item.webkitGetAsEntry(); 1125 | if (entry) { 1126 | // Workaround for Chrome bug #149735: 1127 | entry._file = item.getAsFile(); 1128 | } 1129 | return entry; 1130 | } 1131 | return item.getAsEntry(); 1132 | }) 1133 | ); 1134 | } 1135 | return $.Deferred().resolve( 1136 | $.makeArray(dataTransfer.files) 1137 | ).promise(); 1138 | }, 1139 | 1140 | _getSingleFileInputFiles: function (fileInput) { 1141 | fileInput = $(fileInput); 1142 | var entries = fileInput.prop('webkitEntries') || 1143 | fileInput.prop('entries'), 1144 | files, 1145 | value; 1146 | if (entries && entries.length) { 1147 | return this._handleFileTreeEntries(entries); 1148 | } 1149 | files = $.makeArray(fileInput.prop('files')); 1150 | if (!files.length) { 1151 | value = fileInput.prop('value'); 1152 | if (!value) { 1153 | return $.Deferred().resolve([]).promise(); 1154 | } 1155 | // If the files property is not available, the browser does not 1156 | // support the File API and we add a pseudo File object with 1157 | // the input value as name with path information removed: 1158 | files = [{name: value.replace(/^.*\\/, '')}]; 1159 | } else if (files[0].name === undefined && files[0].fileName) { 1160 | // File normalization for Safari 4 and Firefox 3: 1161 | $.each(files, function (index, file) { 1162 | file.name = file.fileName; 1163 | file.size = file.fileSize; 1164 | }); 1165 | } 1166 | return $.Deferred().resolve(files).promise(); 1167 | }, 1168 | 1169 | _getFileInputFiles: function (fileInput) { 1170 | if (!(fileInput instanceof $) || fileInput.length === 1) { 1171 | return this._getSingleFileInputFiles(fileInput); 1172 | } 1173 | return $.when.apply( 1174 | $, 1175 | $.map(fileInput, this._getSingleFileInputFiles) 1176 | ).pipe(function () { 1177 | return Array.prototype.concat.apply( 1178 | [], 1179 | arguments 1180 | ); 1181 | }); 1182 | }, 1183 | 1184 | _onChange: function (e) { 1185 | var that = this, 1186 | data = { 1187 | fileInput: $(e.target), 1188 | form: $(e.target.form) 1189 | }; 1190 | this._getFileInputFiles(data.fileInput).always(function (files) { 1191 | data.files = files; 1192 | if (that.options.replaceFileInput) { 1193 | that._replaceFileInput(data); 1194 | } 1195 | if (that._trigger( 1196 | 'change', 1197 | $.Event('change', {delegatedEvent: e}), 1198 | data 1199 | ) !== false) { 1200 | that._onAdd(e, data); 1201 | } 1202 | }); 1203 | }, 1204 | 1205 | _onPaste: function (e) { 1206 | var items = e.originalEvent && e.originalEvent.clipboardData && 1207 | e.originalEvent.clipboardData.items, 1208 | data = {files: []}; 1209 | if (items && items.length) { 1210 | $.each(items, function (index, item) { 1211 | var file = item.getAsFile && item.getAsFile(); 1212 | if (file) { 1213 | data.files.push(file); 1214 | } 1215 | }); 1216 | if (this._trigger( 1217 | 'paste', 1218 | $.Event('paste', {delegatedEvent: e}), 1219 | data 1220 | ) !== false) { 1221 | this._onAdd(e, data); 1222 | } 1223 | } 1224 | }, 1225 | 1226 | _onDrop: function (e) { 1227 | e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; 1228 | var that = this, 1229 | dataTransfer = e.dataTransfer, 1230 | data = {}; 1231 | if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { 1232 | e.preventDefault(); 1233 | this._getDroppedFiles(dataTransfer).always(function (files) { 1234 | data.files = files; 1235 | if (that._trigger( 1236 | 'drop', 1237 | $.Event('drop', {delegatedEvent: e}), 1238 | data 1239 | ) !== false) { 1240 | that._onAdd(e, data); 1241 | } 1242 | }); 1243 | } 1244 | }, 1245 | 1246 | _onDragOver: function (e) { 1247 | e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; 1248 | var dataTransfer = e.dataTransfer; 1249 | if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && 1250 | this._trigger( 1251 | 'dragover', 1252 | $.Event('dragover', {delegatedEvent: e}) 1253 | ) !== false) { 1254 | e.preventDefault(); 1255 | dataTransfer.dropEffect = 'copy'; 1256 | } 1257 | }, 1258 | 1259 | _initEventHandlers: function () { 1260 | if (this._isXHRUpload(this.options)) { 1261 | this._on(this.options.dropZone, { 1262 | dragover: this._onDragOver, 1263 | drop: this._onDrop 1264 | }); 1265 | this._on(this.options.pasteZone, { 1266 | paste: this._onPaste 1267 | }); 1268 | } 1269 | if ($.support.fileInput) { 1270 | this._on(this.options.fileInput, { 1271 | change: this._onChange 1272 | }); 1273 | } 1274 | }, 1275 | 1276 | _destroyEventHandlers: function () { 1277 | this._off(this.options.dropZone, 'dragover drop'); 1278 | this._off(this.options.pasteZone, 'paste'); 1279 | this._off(this.options.fileInput, 'change'); 1280 | }, 1281 | 1282 | _setOption: function (key, value) { 1283 | var reinit = $.inArray(key, this._specialOptions) !== -1; 1284 | if (reinit) { 1285 | this._destroyEventHandlers(); 1286 | } 1287 | this._super(key, value); 1288 | if (reinit) { 1289 | this._initSpecialOptions(); 1290 | this._initEventHandlers(); 1291 | } 1292 | }, 1293 | 1294 | _initSpecialOptions: function () { 1295 | var options = this.options; 1296 | if (options.fileInput === undefined) { 1297 | options.fileInput = this.element.is('input[type="file"]') ? 1298 | this.element : this.element.find('input[type="file"]'); 1299 | } else if (!(options.fileInput instanceof $)) { 1300 | options.fileInput = $(options.fileInput); 1301 | } 1302 | if (!(options.dropZone instanceof $)) { 1303 | options.dropZone = $(options.dropZone); 1304 | } 1305 | if (!(options.pasteZone instanceof $)) { 1306 | options.pasteZone = $(options.pasteZone); 1307 | } 1308 | }, 1309 | 1310 | _getRegExp: function (str) { 1311 | var parts = str.split('/'), 1312 | modifiers = parts.pop(); 1313 | parts.shift(); 1314 | return new RegExp(parts.join('/'), modifiers); 1315 | }, 1316 | 1317 | _isRegExpOption: function (key, value) { 1318 | return key !== 'url' && $.type(value) === 'string' && 1319 | /^\/.*\/[igm]{0,3}$/.test(value); 1320 | }, 1321 | 1322 | _initDataAttributes: function () { 1323 | var that = this, 1324 | options = this.options, 1325 | clone = $(this.element[0].cloneNode(false)); 1326 | // Initialize options set via HTML5 data-attributes: 1327 | $.each( 1328 | clone.data(), 1329 | function (key, value) { 1330 | var dataAttributeName = 'data-' + 1331 | // Convert camelCase to hyphen-ated key: 1332 | key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 1333 | if (clone.attr(dataAttributeName)) { 1334 | if (that._isRegExpOption(key, value)) { 1335 | value = that._getRegExp(value); 1336 | } 1337 | options[key] = value; 1338 | } 1339 | } 1340 | ); 1341 | }, 1342 | 1343 | _create: function () { 1344 | this._initDataAttributes(); 1345 | this._initSpecialOptions(); 1346 | this._slots = []; 1347 | this._sequence = this._getXHRPromise(true); 1348 | this._sending = this._active = 0; 1349 | this._initProgressObject(this); 1350 | this._initEventHandlers(); 1351 | }, 1352 | 1353 | // This method is exposed to the widget API and allows to query 1354 | // the number of active uploads: 1355 | active: function () { 1356 | return this._active; 1357 | }, 1358 | 1359 | // This method is exposed to the widget API and allows to query 1360 | // the widget upload progress. 1361 | // It returns an object with loaded, total and bitrate properties 1362 | // for the running uploads: 1363 | progress: function () { 1364 | return this._progress; 1365 | }, 1366 | 1367 | // This method is exposed to the widget API and allows adding files 1368 | // using the fileupload API. The data parameter accepts an object which 1369 | // must have a files property and can contain additional options: 1370 | // .fileupload('add', {files: filesList}); 1371 | add: function (data) { 1372 | var that = this; 1373 | if (!data || this.options.disabled) { 1374 | return; 1375 | } 1376 | if (data.fileInput && !data.files) { 1377 | this._getFileInputFiles(data.fileInput).always(function (files) { 1378 | data.files = files; 1379 | that._onAdd(null, data); 1380 | }); 1381 | } else { 1382 | data.files = $.makeArray(data.files); 1383 | this._onAdd(null, data); 1384 | } 1385 | }, 1386 | 1387 | // This method is exposed to the widget API and allows sending files 1388 | // using the fileupload API. The data parameter accepts an object which 1389 | // must have a files or fileInput property and can contain additional options: 1390 | // .fileupload('send', {files: filesList}); 1391 | // The method returns a Promise object for the file upload call. 1392 | send: function (data) { 1393 | if (data && !this.options.disabled) { 1394 | if (data.fileInput && !data.files) { 1395 | var that = this, 1396 | dfd = $.Deferred(), 1397 | promise = dfd.promise(), 1398 | jqXHR, 1399 | aborted; 1400 | promise.abort = function () { 1401 | aborted = true; 1402 | if (jqXHR) { 1403 | return jqXHR.abort(); 1404 | } 1405 | dfd.reject(null, 'abort', 'abort'); 1406 | return promise; 1407 | }; 1408 | this._getFileInputFiles(data.fileInput).always( 1409 | function (files) { 1410 | if (aborted) { 1411 | return; 1412 | } 1413 | if (!files.length) { 1414 | dfd.reject(); 1415 | return; 1416 | } 1417 | data.files = files; 1418 | jqXHR = that._onSend(null, data); 1419 | jqXHR.then( 1420 | function (result, textStatus, jqXHR) { 1421 | dfd.resolve(result, textStatus, jqXHR); 1422 | }, 1423 | function (jqXHR, textStatus, errorThrown) { 1424 | dfd.reject(jqXHR, textStatus, errorThrown); 1425 | } 1426 | ); 1427 | } 1428 | ); 1429 | return this._enhancePromise(promise); 1430 | } 1431 | data.files = $.makeArray(data.files); 1432 | if (data.files.length) { 1433 | return this._onSend(null, data); 1434 | } 1435 | } 1436 | return this._getXHRPromise(false, data && data.context); 1437 | } 1438 | 1439 | }); 1440 | 1441 | })); 1442 | -------------------------------------------------------------------------------- /static/js/jquery.iframe-transport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Iframe Transport Plugin 1.8.2 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, window, document */ 13 | 14 | (function (factory) { 15 | 'use strict'; 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['jquery'], factory); 19 | } else { 20 | // Browser globals: 21 | factory(window.jQuery); 22 | } 23 | }(function ($) { 24 | 'use strict'; 25 | 26 | // Helper variable to create unique names for the transport iframes: 27 | var counter = 0; 28 | 29 | // The iframe transport accepts four additional options: 30 | // options.fileInput: a jQuery collection of file input fields 31 | // options.paramName: the parameter name for the file form data, 32 | // overrides the name property of the file input field(s), 33 | // can be a string or an array of strings. 34 | // options.formData: an array of objects with name and value properties, 35 | // equivalent to the return data of .serializeArray(), e.g.: 36 | // [{name: 'a', value: 1}, {name: 'b', value: 2}] 37 | // options.initialIframeSrc: the URL of the initial iframe src, 38 | // by default set to "javascript:false;" 39 | $.ajaxTransport('iframe', function (options) { 40 | if (options.async) { 41 | // javascript:false as initial iframe src 42 | // prevents warning popups on HTTPS in IE6: 43 | /*jshint scripturl: true */ 44 | var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', 45 | /*jshint scripturl: false */ 46 | form, 47 | iframe, 48 | addParamChar; 49 | return { 50 | send: function (_, completeCallback) { 51 | form = $('
'); 52 | form.attr('accept-charset', options.formAcceptCharset); 53 | addParamChar = /\?/.test(options.url) ? '&' : '?'; 54 | // XDomainRequest only supports GET and POST: 55 | if (options.type === 'DELETE') { 56 | options.url = options.url + addParamChar + '_method=DELETE'; 57 | options.type = 'POST'; 58 | } else if (options.type === 'PUT') { 59 | options.url = options.url + addParamChar + '_method=PUT'; 60 | options.type = 'POST'; 61 | } else if (options.type === 'PATCH') { 62 | options.url = options.url + addParamChar + '_method=PATCH'; 63 | options.type = 'POST'; 64 | } 65 | // IE versions below IE8 cannot set the name property of 66 | // elements that have already been added to the DOM, 67 | // so we set the name along with the iframe HTML markup: 68 | counter += 1; 69 | iframe = $( 70 | '' 72 | ).bind('load', function () { 73 | var fileInputClones, 74 | paramNames = $.isArray(options.paramName) ? 75 | options.paramName : [options.paramName]; 76 | iframe 77 | .unbind('load') 78 | .bind('load', function () { 79 | var response; 80 | // Wrap in a try/catch block to catch exceptions thrown 81 | // when trying to access cross-domain iframe contents: 82 | try { 83 | response = iframe.contents(); 84 | // Google Chrome and Firefox do not throw an 85 | // exception when calling iframe.contents() on 86 | // cross-domain requests, so we unify the response: 87 | if (!response.length || !response[0].firstChild) { 88 | throw new Error(); 89 | } 90 | } catch (e) { 91 | response = undefined; 92 | } 93 | // The complete callback returns the 94 | // iframe content document as response object: 95 | completeCallback( 96 | 200, 97 | 'success', 98 | {'iframe': response} 99 | ); 100 | // Fix for IE endless progress bar activity bug 101 | // (happens on form submits to iframe targets): 102 | $('') 103 | .appendTo(form); 104 | window.setTimeout(function () { 105 | // Removing the form in a setTimeout call 106 | // allows Chrome's developer tools to display 107 | // the response result 108 | form.remove(); 109 | }, 0); 110 | }); 111 | form 112 | .prop('target', iframe.prop('name')) 113 | .prop('action', options.url) 114 | .prop('method', options.type); 115 | if (options.formData) { 116 | $.each(options.formData, function (index, field) { 117 | $('') 118 | .prop('name', field.name) 119 | .val(field.value) 120 | .appendTo(form); 121 | }); 122 | } 123 | if (options.fileInput && options.fileInput.length && 124 | options.type === 'POST') { 125 | fileInputClones = options.fileInput.clone(); 126 | // Insert a clone for each file input field: 127 | options.fileInput.after(function (index) { 128 | return fileInputClones[index]; 129 | }); 130 | if (options.paramName) { 131 | options.fileInput.each(function (index) { 132 | $(this).prop( 133 | 'name', 134 | paramNames[index] || options.paramName 135 | ); 136 | }); 137 | } 138 | // Appending the file input fields to the hidden form 139 | // removes them from their original location: 140 | form 141 | .append(options.fileInput) 142 | .prop('enctype', 'multipart/form-data') 143 | // enctype must be set as encoding for IE: 144 | .prop('encoding', 'multipart/form-data'); 145 | // Remove the HTML5 form attribute from the input(s): 146 | options.fileInput.removeAttr('form'); 147 | } 148 | form.submit(); 149 | // Insert the file input fields at their original location 150 | // by replacing the clones with the originals: 151 | if (fileInputClones && fileInputClones.length) { 152 | options.fileInput.each(function (index, input) { 153 | var clone = $(fileInputClones[index]); 154 | // Restore the original name and form properties: 155 | $(input) 156 | .prop('name', clone.prop('name')) 157 | .attr('form', clone.attr('form')); 158 | clone.replaceWith(input); 159 | }); 160 | } 161 | }); 162 | form.append(iframe).appendTo(document.body); 163 | }, 164 | abort: function () { 165 | if (iframe) { 166 | // javascript:false as iframe src aborts the request 167 | // and prevents warning popups on HTTPS in IE6. 168 | // concat is used to avoid the "Script URL" JSLint error: 169 | iframe 170 | .unbind('load') 171 | .prop('src', initialIframeSrc); 172 | } 173 | if (form) { 174 | form.remove(); 175 | } 176 | } 177 | }; 178 | } 179 | }); 180 | 181 | // The iframe transport returns the iframe content document as response. 182 | // The following adds converters from iframe to text, json, html, xml 183 | // and script. 184 | // Please note that the Content-Type for JSON responses has to be text/plain 185 | // or text/html, if the browser doesn't include application/json in the 186 | // Accept header, else IE will show a download dialog. 187 | // The Content-Type for XML responses on the other hand has to be always 188 | // application/xml or text/xml, so IE properly parses the XML response. 189 | // See also 190 | // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation 191 | $.ajaxSetup({ 192 | converters: { 193 | 'iframe text': function (iframe) { 194 | return iframe && $(iframe[0].body).text(); 195 | }, 196 | 'iframe json': function (iframe) { 197 | return iframe && $.parseJSON($(iframe[0].body).text()); 198 | }, 199 | 'iframe html': function (iframe) { 200 | return iframe && $(iframe[0].body).html(); 201 | }, 202 | 'iframe xml': function (iframe) { 203 | var xmlDoc = iframe && iframe[0]; 204 | return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : 205 | $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || 206 | $(xmlDoc.body).html()); 207 | }, 208 | 'iframe script': function (iframe) { 209 | return iframe && $.globalEval($(iframe[0].body).text()); 210 | } 211 | } 212 | }); 213 | 214 | })); 215 | -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery File Upload Plugin JS Example 8.9.1 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2010, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global $, window */ 13 | 14 | $(function () { 15 | 'use strict'; 16 | 17 | // Initialize the jQuery File Upload widget: 18 | $('#fileupload').fileupload({ 19 | // Uncomment the following to send cross-domain cookies: 20 | //xhrFields: {withCredentials: true}, 21 | url: 'upload' 22 | }); 23 | 24 | // Enable iframe cross-domain access via redirect option: 25 | $('#fileupload').fileupload( 26 | 'option', 27 | 'redirect', 28 | window.location.href.replace( 29 | /\/[^\/]*$/, 30 | '/cors/result.html?%s' 31 | ) 32 | ); 33 | 34 | if (window.location.hostname === 'blueimp.github.io') { 35 | // Demo settings: 36 | $('#fileupload').fileupload('option', { 37 | url: '//jquery-file-upload.appspot.com/', 38 | // Enable image resizing, except for Android and Opera, 39 | // which actually support image resizing, but fail to 40 | // send Blob objects via XHR requests: 41 | disableImageResize: /Android(?!.*Chrome)|Opera/ 42 | .test(window.navigator.userAgent), 43 | maxFileSize: 5000000, 44 | acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i 45 | }); 46 | // Upload server status check for browsers with CORS support: 47 | if ($.support.cors) { 48 | $.ajax({ 49 | url: '//jquery-file-upload.appspot.com/', 50 | type: 'HEAD' 51 | }).fail(function () { 52 | $('
') 53 | .text('Upload server currently unavailable - ' + 54 | new Date()) 55 | .appendTo('#fileupload'); 56 | }); 57 | } 58 | } else { 59 | // Load existing files: 60 | $('#fileupload').addClass('fileupload-processing'); 61 | $.ajax({ 62 | // Uncomment the following to send cross-domain cookies: 63 | //xhrFields: {withCredentials: true}, 64 | url: $('#fileupload').fileupload('option', 'url'), 65 | dataType: 'json', 66 | context: $('#fileupload')[0] 67 | }).always(function () { 68 | $(this).removeClass('fileupload-processing'); 69 | }).done(function (result) { 70 | $(this).fileupload('option', 'done') 71 | .call(this, $.Event('done'), {result: result}); 72 | }); 73 | } 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /static/js/vendor/jquery.ui.widget.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI Widget 1.10.4+amd 3 | * https://github.com/blueimp/jQuery-File-Upload 4 | * 5 | * Copyright 2014 jQuery Foundation and other contributors 6 | * Released under the MIT license. 7 | * http://jquery.org/license 8 | * 9 | * http://api.jqueryui.com/jQuery.widget/ 10 | */ 11 | 12 | (function (factory) { 13 | if (typeof define === "function" && define.amd) { 14 | // Register as an anonymous AMD module: 15 | define(["jquery"], factory); 16 | } else { 17 | // Browser globals: 18 | factory(jQuery); 19 | } 20 | }(function( $, undefined ) { 21 | 22 | var uuid = 0, 23 | slice = Array.prototype.slice, 24 | _cleanData = $.cleanData; 25 | $.cleanData = function( elems ) { 26 | for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { 27 | try { 28 | $( elem ).triggerHandler( "remove" ); 29 | // http://bugs.jquery.com/ticket/8235 30 | } catch( e ) {} 31 | } 32 | _cleanData( elems ); 33 | }; 34 | 35 | $.widget = function( name, base, prototype ) { 36 | var fullName, existingConstructor, constructor, basePrototype, 37 | // proxiedPrototype allows the provided prototype to remain unmodified 38 | // so that it can be used as a mixin for multiple widgets (#8876) 39 | proxiedPrototype = {}, 40 | namespace = name.split( "." )[ 0 ]; 41 | 42 | name = name.split( "." )[ 1 ]; 43 | fullName = namespace + "-" + name; 44 | 45 | if ( !prototype ) { 46 | prototype = base; 47 | base = $.Widget; 48 | } 49 | 50 | // create selector for plugin 51 | $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { 52 | return !!$.data( elem, fullName ); 53 | }; 54 | 55 | $[ namespace ] = $[ namespace ] || {}; 56 | existingConstructor = $[ namespace ][ name ]; 57 | constructor = $[ namespace ][ name ] = function( options, element ) { 58 | // allow instantiation without "new" keyword 59 | if ( !this._createWidget ) { 60 | return new constructor( options, element ); 61 | } 62 | 63 | // allow instantiation without initializing for simple inheritance 64 | // must use "new" keyword (the code above always passes args) 65 | if ( arguments.length ) { 66 | this._createWidget( options, element ); 67 | } 68 | }; 69 | // extend with the existing constructor to carry over any static properties 70 | $.extend( constructor, existingConstructor, { 71 | version: prototype.version, 72 | // copy the object used to create the prototype in case we need to 73 | // redefine the widget later 74 | _proto: $.extend( {}, prototype ), 75 | // track widgets that inherit from this widget in case this widget is 76 | // redefined after a widget inherits from it 77 | _childConstructors: [] 78 | }); 79 | 80 | basePrototype = new base(); 81 | // we need to make the options hash a property directly on the new instance 82 | // otherwise we'll modify the options hash on the prototype that we're 83 | // inheriting from 84 | basePrototype.options = $.widget.extend( {}, basePrototype.options ); 85 | $.each( prototype, function( prop, value ) { 86 | if ( !$.isFunction( value ) ) { 87 | proxiedPrototype[ prop ] = value; 88 | return; 89 | } 90 | proxiedPrototype[ prop ] = (function() { 91 | var _super = function() { 92 | return base.prototype[ prop ].apply( this, arguments ); 93 | }, 94 | _superApply = function( args ) { 95 | return base.prototype[ prop ].apply( this, args ); 96 | }; 97 | return function() { 98 | var __super = this._super, 99 | __superApply = this._superApply, 100 | returnValue; 101 | 102 | this._super = _super; 103 | this._superApply = _superApply; 104 | 105 | returnValue = value.apply( this, arguments ); 106 | 107 | this._super = __super; 108 | this._superApply = __superApply; 109 | 110 | return returnValue; 111 | }; 112 | })(); 113 | }); 114 | constructor.prototype = $.widget.extend( basePrototype, { 115 | // TODO: remove support for widgetEventPrefix 116 | // always use the name + a colon as the prefix, e.g., draggable:start 117 | // don't prefix for widgets that aren't DOM-based 118 | widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name 119 | }, proxiedPrototype, { 120 | constructor: constructor, 121 | namespace: namespace, 122 | widgetName: name, 123 | widgetFullName: fullName 124 | }); 125 | 126 | // If this widget is being redefined then we need to find all widgets that 127 | // are inheriting from it and redefine all of them so that they inherit from 128 | // the new version of this widget. We're essentially trying to replace one 129 | // level in the prototype chain. 130 | if ( existingConstructor ) { 131 | $.each( existingConstructor._childConstructors, function( i, child ) { 132 | var childPrototype = child.prototype; 133 | 134 | // redefine the child widget using the same prototype that was 135 | // originally used, but inherit from the new version of the base 136 | $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); 137 | }); 138 | // remove the list of existing child constructors from the old constructor 139 | // so the old child constructors can be garbage collected 140 | delete existingConstructor._childConstructors; 141 | } else { 142 | base._childConstructors.push( constructor ); 143 | } 144 | 145 | $.widget.bridge( name, constructor ); 146 | }; 147 | 148 | $.widget.extend = function( target ) { 149 | var input = slice.call( arguments, 1 ), 150 | inputIndex = 0, 151 | inputLength = input.length, 152 | key, 153 | value; 154 | for ( ; inputIndex < inputLength; inputIndex++ ) { 155 | for ( key in input[ inputIndex ] ) { 156 | value = input[ inputIndex ][ key ]; 157 | if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { 158 | // Clone objects 159 | if ( $.isPlainObject( value ) ) { 160 | target[ key ] = $.isPlainObject( target[ key ] ) ? 161 | $.widget.extend( {}, target[ key ], value ) : 162 | // Don't extend strings, arrays, etc. with objects 163 | $.widget.extend( {}, value ); 164 | // Copy everything else by reference 165 | } else { 166 | target[ key ] = value; 167 | } 168 | } 169 | } 170 | } 171 | return target; 172 | }; 173 | 174 | $.widget.bridge = function( name, object ) { 175 | var fullName = object.prototype.widgetFullName || name; 176 | $.fn[ name ] = function( options ) { 177 | var isMethodCall = typeof options === "string", 178 | args = slice.call( arguments, 1 ), 179 | returnValue = this; 180 | 181 | // allow multiple hashes to be passed on init 182 | options = !isMethodCall && args.length ? 183 | $.widget.extend.apply( null, [ options ].concat(args) ) : 184 | options; 185 | 186 | if ( isMethodCall ) { 187 | this.each(function() { 188 | var methodValue, 189 | instance = $.data( this, fullName ); 190 | if ( !instance ) { 191 | return $.error( "cannot call methods on " + name + " prior to initialization; " + 192 | "attempted to call method '" + options + "'" ); 193 | } 194 | if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { 195 | return $.error( "no such method '" + options + "' for " + name + " widget instance" ); 196 | } 197 | methodValue = instance[ options ].apply( instance, args ); 198 | if ( methodValue !== instance && methodValue !== undefined ) { 199 | returnValue = methodValue && methodValue.jquery ? 200 | returnValue.pushStack( methodValue.get() ) : 201 | methodValue; 202 | return false; 203 | } 204 | }); 205 | } else { 206 | this.each(function() { 207 | var instance = $.data( this, fullName ); 208 | if ( instance ) { 209 | instance.option( options || {} )._init(); 210 | } else { 211 | $.data( this, fullName, new object( options, this ) ); 212 | } 213 | }); 214 | } 215 | 216 | return returnValue; 217 | }; 218 | }; 219 | 220 | $.Widget = function( /* options, element */ ) {}; 221 | $.Widget._childConstructors = []; 222 | 223 | $.Widget.prototype = { 224 | widgetName: "widget", 225 | widgetEventPrefix: "", 226 | defaultElement: "
", 227 | options: { 228 | disabled: false, 229 | 230 | // callbacks 231 | create: null 232 | }, 233 | _createWidget: function( options, element ) { 234 | element = $( element || this.defaultElement || this )[ 0 ]; 235 | this.element = $( element ); 236 | this.uuid = uuid++; 237 | this.eventNamespace = "." + this.widgetName + this.uuid; 238 | this.options = $.widget.extend( {}, 239 | this.options, 240 | this._getCreateOptions(), 241 | options ); 242 | 243 | this.bindings = $(); 244 | this.hoverable = $(); 245 | this.focusable = $(); 246 | 247 | if ( element !== this ) { 248 | $.data( element, this.widgetFullName, this ); 249 | this._on( true, this.element, { 250 | remove: function( event ) { 251 | if ( event.target === element ) { 252 | this.destroy(); 253 | } 254 | } 255 | }); 256 | this.document = $( element.style ? 257 | // element within the document 258 | element.ownerDocument : 259 | // element is window or document 260 | element.document || element ); 261 | this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); 262 | } 263 | 264 | this._create(); 265 | this._trigger( "create", null, this._getCreateEventData() ); 266 | this._init(); 267 | }, 268 | _getCreateOptions: $.noop, 269 | _getCreateEventData: $.noop, 270 | _create: $.noop, 271 | _init: $.noop, 272 | 273 | destroy: function() { 274 | this._destroy(); 275 | // we can probably remove the unbind calls in 2.0 276 | // all event bindings should go through this._on() 277 | this.element 278 | .unbind( this.eventNamespace ) 279 | // 1.9 BC for #7810 280 | // TODO remove dual storage 281 | .removeData( this.widgetName ) 282 | .removeData( this.widgetFullName ) 283 | // support: jquery <1.6.3 284 | // http://bugs.jquery.com/ticket/9413 285 | .removeData( $.camelCase( this.widgetFullName ) ); 286 | this.widget() 287 | .unbind( this.eventNamespace ) 288 | .removeAttr( "aria-disabled" ) 289 | .removeClass( 290 | this.widgetFullName + "-disabled " + 291 | "ui-state-disabled" ); 292 | 293 | // clean up events and states 294 | this.bindings.unbind( this.eventNamespace ); 295 | this.hoverable.removeClass( "ui-state-hover" ); 296 | this.focusable.removeClass( "ui-state-focus" ); 297 | }, 298 | _destroy: $.noop, 299 | 300 | widget: function() { 301 | return this.element; 302 | }, 303 | 304 | option: function( key, value ) { 305 | var options = key, 306 | parts, 307 | curOption, 308 | i; 309 | 310 | if ( arguments.length === 0 ) { 311 | // don't return a reference to the internal hash 312 | return $.widget.extend( {}, this.options ); 313 | } 314 | 315 | if ( typeof key === "string" ) { 316 | // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } 317 | options = {}; 318 | parts = key.split( "." ); 319 | key = parts.shift(); 320 | if ( parts.length ) { 321 | curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); 322 | for ( i = 0; i < parts.length - 1; i++ ) { 323 | curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; 324 | curOption = curOption[ parts[ i ] ]; 325 | } 326 | key = parts.pop(); 327 | if ( arguments.length === 1 ) { 328 | return curOption[ key ] === undefined ? null : curOption[ key ]; 329 | } 330 | curOption[ key ] = value; 331 | } else { 332 | if ( arguments.length === 1 ) { 333 | return this.options[ key ] === undefined ? null : this.options[ key ]; 334 | } 335 | options[ key ] = value; 336 | } 337 | } 338 | 339 | this._setOptions( options ); 340 | 341 | return this; 342 | }, 343 | _setOptions: function( options ) { 344 | var key; 345 | 346 | for ( key in options ) { 347 | this._setOption( key, options[ key ] ); 348 | } 349 | 350 | return this; 351 | }, 352 | _setOption: function( key, value ) { 353 | this.options[ key ] = value; 354 | 355 | if ( key === "disabled" ) { 356 | this.widget() 357 | .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) 358 | .attr( "aria-disabled", value ); 359 | this.hoverable.removeClass( "ui-state-hover" ); 360 | this.focusable.removeClass( "ui-state-focus" ); 361 | } 362 | 363 | return this; 364 | }, 365 | 366 | enable: function() { 367 | return this._setOption( "disabled", false ); 368 | }, 369 | disable: function() { 370 | return this._setOption( "disabled", true ); 371 | }, 372 | 373 | _on: function( suppressDisabledCheck, element, handlers ) { 374 | var delegateElement, 375 | instance = this; 376 | 377 | // no suppressDisabledCheck flag, shuffle arguments 378 | if ( typeof suppressDisabledCheck !== "boolean" ) { 379 | handlers = element; 380 | element = suppressDisabledCheck; 381 | suppressDisabledCheck = false; 382 | } 383 | 384 | // no element argument, shuffle and use this.element 385 | if ( !handlers ) { 386 | handlers = element; 387 | element = this.element; 388 | delegateElement = this.widget(); 389 | } else { 390 | // accept selectors, DOM elements 391 | element = delegateElement = $( element ); 392 | this.bindings = this.bindings.add( element ); 393 | } 394 | 395 | $.each( handlers, function( event, handler ) { 396 | function handlerProxy() { 397 | // allow widgets to customize the disabled handling 398 | // - disabled as an array instead of boolean 399 | // - disabled class as method for disabling individual parts 400 | if ( !suppressDisabledCheck && 401 | ( instance.options.disabled === true || 402 | $( this ).hasClass( "ui-state-disabled" ) ) ) { 403 | return; 404 | } 405 | return ( typeof handler === "string" ? instance[ handler ] : handler ) 406 | .apply( instance, arguments ); 407 | } 408 | 409 | // copy the guid so direct unbinding works 410 | if ( typeof handler !== "string" ) { 411 | handlerProxy.guid = handler.guid = 412 | handler.guid || handlerProxy.guid || $.guid++; 413 | } 414 | 415 | var match = event.match( /^(\w+)\s*(.*)$/ ), 416 | eventName = match[1] + instance.eventNamespace, 417 | selector = match[2]; 418 | if ( selector ) { 419 | delegateElement.delegate( selector, eventName, handlerProxy ); 420 | } else { 421 | element.bind( eventName, handlerProxy ); 422 | } 423 | }); 424 | }, 425 | 426 | _off: function( element, eventName ) { 427 | eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; 428 | element.unbind( eventName ).undelegate( eventName ); 429 | }, 430 | 431 | _delay: function( handler, delay ) { 432 | function handlerProxy() { 433 | return ( typeof handler === "string" ? instance[ handler ] : handler ) 434 | .apply( instance, arguments ); 435 | } 436 | var instance = this; 437 | return setTimeout( handlerProxy, delay || 0 ); 438 | }, 439 | 440 | _hoverable: function( element ) { 441 | this.hoverable = this.hoverable.add( element ); 442 | this._on( element, { 443 | mouseenter: function( event ) { 444 | $( event.currentTarget ).addClass( "ui-state-hover" ); 445 | }, 446 | mouseleave: function( event ) { 447 | $( event.currentTarget ).removeClass( "ui-state-hover" ); 448 | } 449 | }); 450 | }, 451 | 452 | _focusable: function( element ) { 453 | this.focusable = this.focusable.add( element ); 454 | this._on( element, { 455 | focusin: function( event ) { 456 | $( event.currentTarget ).addClass( "ui-state-focus" ); 457 | }, 458 | focusout: function( event ) { 459 | $( event.currentTarget ).removeClass( "ui-state-focus" ); 460 | } 461 | }); 462 | }, 463 | 464 | _trigger: function( type, event, data ) { 465 | var prop, orig, 466 | callback = this.options[ type ]; 467 | 468 | data = data || {}; 469 | event = $.Event( event ); 470 | event.type = ( type === this.widgetEventPrefix ? 471 | type : 472 | this.widgetEventPrefix + type ).toLowerCase(); 473 | // the original event may come from any element 474 | // so we need to reset the target on the new event 475 | event.target = this.element[ 0 ]; 476 | 477 | // copy original event properties over to the new event 478 | orig = event.originalEvent; 479 | if ( orig ) { 480 | for ( prop in orig ) { 481 | if ( !( prop in event ) ) { 482 | event[ prop ] = orig[ prop ]; 483 | } 484 | } 485 | } 486 | 487 | this.element.trigger( event, data ); 488 | return !( $.isFunction( callback ) && 489 | callback.apply( this.element[0], [ event ].concat( data ) ) === false || 490 | event.isDefaultPrevented() ); 491 | } 492 | }; 493 | 494 | $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { 495 | $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { 496 | if ( typeof options === "string" ) { 497 | options = { effect: options }; 498 | } 499 | var hasOptions, 500 | effectName = !options ? 501 | method : 502 | options === true || typeof options === "number" ? 503 | defaultEffect : 504 | options.effect || defaultEffect; 505 | options = options || {}; 506 | if ( typeof options === "number" ) { 507 | options = { duration: options }; 508 | } 509 | hasOptions = !$.isEmptyObject( options ); 510 | options.complete = callback; 511 | if ( options.delay ) { 512 | element.delay( options.delay ); 513 | } 514 | if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { 515 | element[ method ]( options ); 516 | } else if ( effectName !== method && element[ effectName ] ) { 517 | element[ effectName ]( options.duration, options.easing, callback ); 518 | } else { 519 | element.queue(function( next ) { 520 | $( this )[ method ](); 521 | if ( callback ) { 522 | callback.call( element[ 0 ] ); 523 | } 524 | next(); 525 | }); 526 | } 527 | }; 528 | }); 529 | 530 | })); 531 | -------------------------------------------------------------------------------- /system_package.txt: -------------------------------------------------------------------------------- 1 | # system package requirements 2 | 3 | # ubuntu / debian 4 | sudo apt-get install libjpeg-dev 5 | 6 | # centos 7 | sudo yum install -y libjpeg-devel -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}File Uploader - Page Not Found{% endblock %} 4 | 5 | {% block page_content %} 6 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}File Uploader - Internal Server Error{% endblock %} 4 | 5 | {% block page_content %} 6 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | 3 | {% block title %}File Uploader{% endblock %} 4 | 5 | {% block head %} 6 | {{ super() }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% endblock %} 20 | 21 | {% block navbar %} 22 | 43 | {% endblock %} 44 | 45 | {% block content %} 46 | {% block page_content %}{% endblock %} 47 | {% endblock %} 48 | 49 | {% block scripts %} 50 | {{ super() }} 51 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}File Uploader{% endblock %} 3 | 4 | {% block page_content %} 5 |
6 |

File Uploader

7 |
8 |
9 |

File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery.
10 | Supports cross-domain, chunked and resumable file uploads and client-side image resizing.
11 | Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.

12 |
13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | Add files... 25 | 26 | 27 | 31 | 35 | 39 | 40 | 41 | 42 |
43 | 44 |
45 | 46 |
47 |
48 |
49 | 50 |
 
51 |
52 |
53 | 54 | 55 |
56 |
57 |
58 |
59 |

Notes

60 |
61 |
62 |
    63 |
  • The maximum file size for uploads in this demo is 50 MB (default file size is unlimited).
  • 64 |
  • Only image files (TXT, JPG, GIF, PNG, BMP, RAR, ZIP, 7ZIP, DOC, DOCX) are allowed in this website.
  • 65 | 66 |
  • You can drag & drop files from your desktop on this webpage (see Browser support).
  • 67 |
  • Please refer to the project website and documentation for more information.
  • 68 |
  • Built with Twitter's Bootstrap CSS framework and Icons from Glyphicons.
  • 69 |
70 |
71 |
72 |
73 | 74 | 83 | 84 | {% raw %} 85 | 86 | 117 | 118 | 160 | {% endraw %} 161 | {% endblock %} 162 | 163 | {% block scripts %} 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 199 | {% endblock %} 200 | --------------------------------------------------------------------------------