From 1205e3539d56df396808d03f754440964cf94be5 Mon Sep 17 00:00:00 2001 From: Denis Fedoseev Date: Fri, 4 Aug 2017 09:57:17 +0300 Subject: [PATCH 1/9] Pagination mostly works --- fotostore.pl | 23 +++++++++++-- lib/FotoStore/DB.pm | 14 ++++++-- templates/includes/images_list.html.ep | 45 ++++++++++++++++++-------- templates/layouts/base.html.ep | 1 + 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/fotostore.pl b/fotostore.pl index e2c7abe..7f09911 100755 --- a/fotostore.pl +++ b/fotostore.pl @@ -9,6 +9,7 @@ use File::Basename 'basename'; use File::Path 'mkpath'; use File::Spec 'catfile'; use Cwd; +use POSIX; use Imager; use DBI; @@ -141,10 +142,21 @@ get '/' => sub { get '/get_images' => ( authenticated => 1 ) => sub { my $self = shift; + #Getting current user my $current_user = $self->current_user; my $user_id = $current_user->{'user_id'}; - my $files_list = $db->get_files( $current_user->{'user_id'}, 20 ); + #Getting images list with paging + my $page = $self->param('page') || 1; + my $items = $self->param('per-page') || 20; + + if (($page !~ /^\d+$/) || ($page <= 1)) { $page = 1} + if (($items !~ /^\d+$/) || ($items <= 0)) { $items = 20} + + # process images list + my $req_result = $db->get_files( $current_user->{'user_id'}, $items , $page); + my $files_list = $req_result->{'images_list'}; + my $pages_count = ceil($req_result->{'total_rows'}/$items); my $thumbs_dir = File::Spec->catfile( $IMAGE_DIR, $current_user->{'user_id'}, @@ -157,6 +169,7 @@ get '/get_images' => ( authenticated => 1 ) => sub { for my $img_item (@$files_list) { my $file = $img_item->{'file_name'}; my $img_hash = {}; + $img_hash->{'id'} = $img_item->{'file_id'}; $img_hash->{'filename'} = $img_item->{'original_filename'}; $img_hash->{'original_url'} = File::Spec->catfile( '/', $IMAGE_BASE, $current_user->{'user_id'}, @@ -185,10 +198,14 @@ get '/get_images' => ( authenticated => 1 ) => sub { $img_hash->{'scales'} = \@scaled; push( @$images, $img_hash ); + + } + my $reply_data = { current_page => $page, items_per_page => $items, pages_count => $pages_count, images_list => $images }; + # Render - return $self->render( json => $images ); + return $self->render( json => $reply_data ); }; # Upload image file @@ -201,7 +218,7 @@ post '/upload' => ( authenticated => 1 ) => sub { my $user = $self->current_user(); my $user_id = $user->{'user_id'}; - $self->app->log->debug( "user:" . Dumper($user) ); + # $self->app->log->debug( "user:" . Dumper($user) ); # Not upload unless ($image) { diff --git a/lib/FotoStore/DB.pm b/lib/FotoStore/DB.pm index 49dea87..4951676 100644 --- a/lib/FotoStore/DB.pm +++ b/lib/FotoStore/DB.pm @@ -1,5 +1,6 @@ package FotoStore::DB; +use v5.20; use strict; use warnings; @@ -59,8 +60,17 @@ sub add_file($self, $user_id, $filename, $original_filename) { return $rows; } -sub get_files($self, $user_id, $count=20, $start_at=0) { - return $self->{'dbh'}->selectall_arrayref(q~select * from images where owner_id=? order by created_time desc~, { Slice => {} }, $user_id ); +sub get_files($self, $user_id, $items_count=20, $page=1) { + + # Calculate offset + # Pages in UI starts from 1, but here we need it to start from 0 + $page = 1 if ($page < 1); + my $start_at = --$page * $items_count; + + my ($rows_count) = $self->{'dbh'}->selectrow_array(q~select count(*) from images where owner_id=? ~, undef , $user_id); + my $images_list = $self->{'dbh'}->selectall_arrayref(q~select * from images where owner_id=? order by created_time desc LIMIT ? OFFSET ? ~, { Slice => {} }, $user_id, $items_count, $start_at ); + + return { total_rows => $rows_count, images_list => $images_list }; } 1; \ No newline at end of file diff --git a/templates/includes/images_list.html.ep b/templates/includes/images_list.html.ep index 5649542..5f8cfa2 100644 --- a/templates/includes/images_list.html.ep +++ b/templates/includes/images_list.html.ep @@ -39,7 +39,7 @@
-
{{ image.filename }}
+
{{ image.id }} - {{ image.filename }}
    @@ -50,7 +50,20 @@
+
+
+ + + +
+
+
Fotos per page: + +
+
+
+ \ No newline at end of file diff --git a/templates/layouts/base.html.ep b/templates/layouts/base.html.ep index a3c18a5..3dd0510 100644 --- a/templates/layouts/base.html.ep +++ b/templates/layouts/base.html.ep @@ -15,6 +15,7 @@ + - - - -
-

jQuery File Upload Demo

-

AngularJS version

- -
-
-

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

-
-
- -
- - - -
-
- - - - Add files... - - - - - - -
- -
- -
- -
 
-
-
- - - - - - - - -
-
- -
-
-
-

- - {{file.name}} - {{file.name}} - - {{file.name}} -

- {{file.error}} -
-

{{file.size | formatFileSize}}

-
-
- - - -
-
-
-
-
-

Demo Notes

-
-
-
    -
  • The maximum file size for uploads in this demo is 999 KB (default file size is unlimited).
  • -
  • Only image files (JPG, GIF, PNG) are allowed in this demo (by default there is no file type restriction).
  • -
  • Uploaded files will be deleted automatically after 5 minutes or less (demo files are stored in memory).
  • -
  • You can drag & drop files from your desktop on this webpage (see Browser support).
  • -
  • Please refer to the project website and documentation for more information.
  • -
  • Built with the Bootstrap CSS framework and Icons from Glyphicons.
  • -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/file_uploader/basic-plus.html b/public/file_uploader/basic-plus.html deleted file mode 100755 index 9e5c232..0000000 --- a/public/file_uploader/basic-plus.html +++ /dev/null @@ -1,226 +0,0 @@ - - - - - - - -jQuery File Upload Demo - Basic Plus version - - - - - - - - - - - -
-

jQuery File Upload Demo

-

Basic Plus version

- -
-
-

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

-
-
- - - - Add files... - - - -
-
- -
-
-
- -
-
-
-
-

Demo Notes

-
-
-
    -
  • The maximum file size for uploads in this demo is 999 KB (default file size is unlimited).
  • -
  • Only image files (JPG, GIF, PNG) are allowed in this demo (by default there is no file type restriction).
  • -
  • Uploaded files will be deleted automatically after 5 minutes or less (demo files are stored in memory).
  • -
  • You can drag & drop files from your desktop on this webpage (see Browser support).
  • -
  • Please refer to the project website and documentation for more information.
  • -
  • Built with the Bootstrap CSS framework and Icons from Glyphicons.
  • -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/file_uploader/basic.html b/public/file_uploader/basic.html deleted file mode 100755 index c0df639..0000000 --- a/public/file_uploader/basic.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - -jQuery File Upload Demo - Basic version - - - - - - - - - - - -
-

jQuery File Upload Demo

-

Basic version

- -
-
-

File Upload widget with multiple file selection, drag&drop support and progress bar for jQuery.
- Supports cross-domain, chunked and resumable file uploads.
- Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads.

-
-
- - - - Select files... - - - -
-
- -
-
-
- -
-
-
-
-

Demo Notes

-
-
-
    -
  • The maximum file size for uploads in this demo is 999 KB (default file size is unlimited).
  • -
  • Only image files (JPG, GIF, PNG) are allowed in this demo (by default there is no file type restriction).
  • -
  • Uploaded files will be deleted automatically after 5 minutes or less (demo files are stored in memory).
  • -
  • You can drag & drop files from your desktop on this webpage (see Browser support).
  • -
  • Please refer to the project website and documentation for more information.
  • -
  • Built with the Bootstrap CSS framework and Icons from Glyphicons.
  • -
-
-
-
- - - - - - - - - - - - diff --git a/public/file_uploader/bower-version-update.js b/public/file_uploader/bower-version-update.js deleted file mode 100755 index 09ce392..0000000 --- a/public/file_uploader/bower-version-update.js +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -var path = require('path'); -var packageJSON = require(path.join(__dirname, 'package.json')); -var bowerFile = path.join(__dirname, 'bower.json'); -var bowerJSON = require('bower-json').parse( - require(bowerFile), - {normalize: true} -); -bowerJSON.version = packageJSON.version; -require('fs').writeFileSync( - bowerFile, - JSON.stringify(bowerJSON, null, 2) + '\n' -); diff --git a/public/file_uploader/bower.json b/public/file_uploader/bower.json deleted file mode 100755 index 90c74c7..0000000 --- a/public/file_uploader/bower.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "blueimp-file-upload", - "version": "9.18.0", - "title": "jQuery File Upload", - "description": "File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images.", - "keywords": [ - "jquery", - "file", - "upload", - "widget", - "multiple", - "selection", - "drag", - "drop", - "progress", - "preview", - "cross-domain", - "cross-site", - "chunk", - "resume", - "gae", - "go", - "python", - "php", - "bootstrap" - ], - "homepage": "https://github.com/blueimp/jQuery-File-Upload", - "author": { - "name": "Sebastian Tschan", - "url": "https://blueimp.net" - }, - "maintainers": [ - { - "name": "Sebastian Tschan", - "url": "https://blueimp.net" - } - ], - "repository": { - "type": "git", - "url": "git://github.com/blueimp/jQuery-File-Upload.git" - }, - "bugs": "https://github.com/blueimp/jQuery-File-Upload/issues", - "license": "MIT", - "dependencies": { - "jquery": ">=1.6", - "blueimp-tmpl": ">=2.5.4", - "blueimp-load-image": ">=1.13.0", - "blueimp-canvas-to-blob": ">=2.1.1" - }, - "main": [ - "js/jquery.fileupload.js" - ], - "ignore": [ - "/*.*", - "/cors", - "css/demo-ie8.css", - "css/demo.css", - "css/style.css", - "js/app.js", - "js/main.js", - "server", - "test" - ] -} diff --git a/public/file_uploader/cors/postmessage.html b/public/file_uploader/cors/postmessage.html deleted file mode 100755 index 6db288c..0000000 --- a/public/file_uploader/cors/postmessage.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - -jQuery File Upload Plugin postMessage API - - - - - - diff --git a/public/file_uploader/cors/result.html b/public/file_uploader/cors/result.html deleted file mode 100755 index e3d6298..0000000 --- a/public/file_uploader/cors/result.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - -jQuery Iframe Transport Plugin Redirect Page - - - - - diff --git a/public/file_uploader/css/demo-ie8.css b/public/file_uploader/css/jquery-ui-demo-ie8.css old mode 100755 new mode 100644 similarity index 100% rename from public/file_uploader/css/demo-ie8.css rename to public/file_uploader/css/jquery-ui-demo-ie8.css diff --git a/public/file_uploader/css/demo.css b/public/file_uploader/css/jquery-ui-demo.css old mode 100755 new mode 100644 similarity index 100% rename from public/file_uploader/css/demo.css rename to public/file_uploader/css/jquery-ui-demo.css diff --git a/public/file_uploader/css/jquery.fileupload-noscript.css b/public/file_uploader/css/jquery.fileupload-noscript.css old mode 100755 new mode 100644 diff --git a/public/file_uploader/css/jquery.fileupload-ui-noscript.css b/public/file_uploader/css/jquery.fileupload-ui-noscript.css old mode 100755 new mode 100644 diff --git a/public/file_uploader/css/jquery.fileupload-ui.css b/public/file_uploader/css/jquery.fileupload-ui.css old mode 100755 new mode 100644 diff --git a/public/file_uploader/css/jquery.fileupload.css b/public/file_uploader/css/jquery.fileupload.css old mode 100755 new mode 100644 diff --git a/public/file_uploader/css/style.css b/public/file_uploader/css/style.css old mode 100755 new mode 100644 diff --git a/public/file_uploader/img/loading.gif b/public/file_uploader/img/loading.gif old mode 100755 new mode 100644 diff --git a/public/file_uploader/img/progressbar.gif b/public/file_uploader/img/progressbar.gif old mode 100755 new mode 100644 diff --git a/public/file_uploader/index.html b/public/file_uploader/index.html deleted file mode 100755 index 2a8dc15..0000000 --- a/public/file_uploader/index.html +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - - -jQuery File Upload Demo - - - - - - - - - - - - - - - - - -
-

jQuery File Upload Demo

-

Basic Plus UI version

- -
-
-

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

-
-
- -
- - - -
-
- - - - Add files... - - - - - - - - -
- -
- -
-
-
- -
 
-
-
- - -
-
-
-
-

Demo Notes

-
-
-
    -
  • The maximum file size for uploads in this demo is 999 KB (default file size is unlimited).
  • -
  • Only image files (JPG, GIF, PNG) are allowed in this demo (by default there is no file type restriction).
  • -
  • Uploaded files will be deleted automatically after 5 minutes or less (demo files are stored in memory).
  • -
  • You can drag & drop files from your desktop on this webpage (see Browser support).
  • -
  • Please refer to the project website and documentation for more information.
  • -
  • Built with the Bootstrap CSS framework and Icons from Glyphicons.
  • -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/file_uploader/jquery-ui.html b/public/file_uploader/jquery-ui.html deleted file mode 100755 index 83fe9ac..0000000 --- a/public/file_uploader/jquery-ui.html +++ /dev/null @@ -1,250 +0,0 @@ - - - - - - - -jQuery File Upload Demo - jQuery UI version - - - - - - - - - - - - - - - - - - - -

jQuery File Upload Demo

-

jQuery UI version

-
- - -
- -
-

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

-
- -
- - - -
-
- - - Add files... - - - - - - - - -
- - -
- -
-
-
-

Demo Notes

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/file_uploader/js/app.js b/public/file_uploader/js/app.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/cors/jquery.postmessage-transport.js b/public/file_uploader/js/cors/jquery.postmessage-transport.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/cors/jquery.xdr-transport.js b/public/file_uploader/js/cors/jquery.xdr-transport.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/jquery.fileupload-angular.js b/public/file_uploader/js/jquery.fileupload-angular.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/jquery.fileupload-audio.js b/public/file_uploader/js/jquery.fileupload-audio.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/jquery.fileupload-image.js b/public/file_uploader/js/jquery.fileupload-image.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/jquery.fileupload-jquery-ui.js b/public/file_uploader/js/jquery.fileupload-jquery-ui.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/jquery.fileupload-process.js b/public/file_uploader/js/jquery.fileupload-process.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/jquery.fileupload-ui.js b/public/file_uploader/js/jquery.fileupload-ui.js old mode 100755 new mode 100644 index 83e7449..5058084 --- a/public/file_uploader/js/jquery.fileupload-ui.js +++ b/public/file_uploader/js/jquery.fileupload-ui.js @@ -30,6 +30,7 @@ require('jquery'), require('blueimp-tmpl'), require('./jquery.fileupload-image'), + require('./jquery.fileupload-audio'), require('./jquery.fileupload-video'), require('./jquery.fileupload-validate') ); diff --git a/public/file_uploader/js/jquery.fileupload-validate.js b/public/file_uploader/js/jquery.fileupload-validate.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/jquery.fileupload-video.js b/public/file_uploader/js/jquery.fileupload-video.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/jquery.fileupload.js b/public/file_uploader/js/jquery.fileupload.js old mode 100755 new mode 100644 index 5ff151b..629f57a --- a/public/file_uploader/js/jquery.fileupload.js +++ b/public/file_uploader/js/jquery.fileupload.js @@ -43,7 +43,7 @@ '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' ).test(window.navigator.userAgent) || // Feature detection for all other devices: - $('').prop('disabled')); + $('').prop('disabled')); // The FileReader API is not actually used, but works as feature detection, // as some Safari versions (5?) support XHR file uploads via the FormData API, @@ -453,7 +453,7 @@ } if (!multipart || options.blob || !this._isInstanceOf('File', file)) { options.headers['Content-Disposition'] = 'attachment; filename="' + - encodeURI(file.name) + '"'; + encodeURI(file.uploadName || file.name) + '"'; } if (!multipart) { options.contentType = file.type || 'application/octet-stream'; @@ -489,7 +489,11 @@ }); } if (options.blob) { - formData.append(paramName, options.blob, file.name); + formData.append( + paramName, + options.blob, + file.uploadName || file.name + ); } else { $.each(options.files, function (index, file) { // This check allows the tests to run with @@ -730,7 +734,7 @@ promise = dfd.promise(), jqXHR, upload; - if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || + if (!(this._isXHRUpload(options) && slice && (ub || ($.type(mcs) === 'function' ? mcs(options) : mcs) < fs)) || options.data) { return false; } @@ -753,7 +757,7 @@ o.blob = slice.call( file, ub, - ub + mcs, + ub + ($.type(mcs) === 'function' ? mcs(o) : mcs), file.type ); // Store the current chunk size, as the blob itself @@ -1126,7 +1130,7 @@ dirReader = entry.createReader(); readEntries(); } else { - // Return an empy list for file system items + // Return an empty list for file system items // other than files or directories: dfd.resolve([]); } diff --git a/public/file_uploader/js/jquery.iframe-transport.js b/public/file_uploader/js/jquery.iframe-transport.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/main.js b/public/file_uploader/js/main.js old mode 100755 new mode 100644 diff --git a/public/file_uploader/js/vendor/jquery.ui.widget.js b/public/file_uploader/js/vendor/jquery.ui.widget.js old mode 100755 new mode 100644 index e08df3f..914b8ff --- a/public/file_uploader/js/vendor/jquery.ui.widget.js +++ b/public/file_uploader/js/vendor/jquery.ui.widget.js @@ -1,571 +1,751 @@ -/*! jQuery UI - v1.11.4+CommonJS - 2015-08-28 -* http://jqueryui.com -* Includes: widget.js -* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ +/*! jQuery UI - v1.12.1+CommonJS - 2018-02-10 + * http://jqueryui.com + * Includes: widget.js + * Copyright jQuery Foundation and other contributors; Licensed MIT */ (function( factory ) { - if ( typeof define === "function" && define.amd ) { + if ( typeof define === "function" && define.amd ) { - // AMD. Register as an anonymous module. - define([ "jquery" ], factory ); + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + } else if ( typeof exports === "object" ) { - } else if ( typeof exports === "object" ) { + // Node/CommonJS + factory( require( "jquery" ) ); + } else { - // Node/CommonJS - factory( require( "jquery" ) ); - - } else { - - // Browser globals - factory( jQuery ); - } + // Browser globals + factory( jQuery ); + } }(function( $ ) { -/*! - * jQuery UI Widget 1.11.4 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - * - * http://api.jqueryui.com/jQuery.widget/ - */ + + $.ui = $.ui || {}; + + var version = $.ui.version = "1.12.1"; -var widget_uuid = 0, - widget_slice = Array.prototype.slice; + /*! + * jQuery UI Widget 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ -$.cleanData = (function( orig ) { - return function( elems ) { - var events, elem, i; - for ( i = 0; (elem = elems[i]) != null; i++ ) { - try { + //>>label: Widget + //>>group: Core + //>>description: Provides a factory for creating stateful widgets with a common API. + //>>docs: http://api.jqueryui.com/jQuery.widget/ + //>>demos: http://jqueryui.com/widget/ - // Only trigger remove when necessary to save time - events = $._data( elem, "events" ); - if ( events && events.remove ) { - $( elem ).triggerHandler( "remove" ); - } - // http://bugs.jquery.com/ticket/8235 - } catch ( e ) {} - } - orig( elems ); - }; -})( $.cleanData ); -$.widget = function( name, base, prototype ) { - var fullName, existingConstructor, constructor, basePrototype, - // proxiedPrototype allows the provided prototype to remain unmodified - // so that it can be used as a mixin for multiple widgets (#8876) - proxiedPrototype = {}, - namespace = name.split( "." )[ 0 ]; + var widgetUuid = 0; + var widgetSlice = Array.prototype.slice; - name = name.split( "." )[ 1 ]; - fullName = namespace + "-" + name; + $.cleanData = ( function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { + try { - if ( !prototype ) { - prototype = base; - base = $.Widget; - } + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } - // create selector for plugin - $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { - return !!$.data( elem, fullName ); - }; + // Http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; + } )( $.cleanData ); - $[ namespace ] = $[ namespace ] || {}; - existingConstructor = $[ namespace ][ name ]; - constructor = $[ namespace ][ name ] = function( options, element ) { - // allow instantiation without "new" keyword - if ( !this._createWidget ) { - return new constructor( options, element ); - } + $.widget = function( name, base, prototype ) { + var existingConstructor, constructor, basePrototype; - // allow instantiation without initializing for simple inheritance - // must use "new" keyword (the code above always passes args) - if ( arguments.length ) { - this._createWidget( options, element ); - } - }; - // extend with the existing constructor to carry over any static properties - $.extend( constructor, existingConstructor, { - version: prototype.version, - // copy the object used to create the prototype in case we need to - // redefine the widget later - _proto: $.extend( {}, prototype ), - // track widgets that inherit from this widget in case this widget is - // redefined after a widget inherits from it - _childConstructors: [] - }); + // ProxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + var proxiedPrototype = {}; - basePrototype = new base(); - // we need to make the options hash a property directly on the new instance - // otherwise we'll modify the options hash on the prototype that we're - // inheriting from - basePrototype.options = $.widget.extend( {}, basePrototype.options ); - $.each( prototype, function( prop, value ) { - if ( !$.isFunction( value ) ) { - proxiedPrototype[ prop ] = value; - return; - } - proxiedPrototype[ prop ] = (function() { - var _super = function() { - return base.prototype[ prop ].apply( this, arguments ); - }, - _superApply = function( args ) { - return base.prototype[ prop ].apply( this, args ); - }; - return function() { - var __super = this._super, - __superApply = this._superApply, - returnValue; + var namespace = name.split( "." )[ 0 ]; + name = name.split( "." )[ 1 ]; + var fullName = namespace + "-" + name; - this._super = _super; - this._superApply = _superApply; + if ( !prototype ) { + prototype = base; + base = $.Widget; + } - returnValue = value.apply( this, arguments ); + if ( $.isArray( prototype ) ) { + prototype = $.extend.apply( null, [ {} ].concat( prototype ) ); + } - this._super = __super; - this._superApply = __superApply; + // Create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; - return returnValue; - }; - })(); - }); - constructor.prototype = $.widget.extend( basePrototype, { - // TODO: remove support for widgetEventPrefix - // always use the name + a colon as the prefix, e.g., draggable:start - // don't prefix for widgets that aren't DOM-based - widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name - }, proxiedPrototype, { - constructor: constructor, - namespace: namespace, - widgetName: name, - widgetFullName: fullName - }); + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { - // If this widget is being redefined then we need to find all widgets that - // are inheriting from it and redefine all of them so that they inherit from - // the new version of this widget. We're essentially trying to replace one - // level in the prototype chain. - if ( existingConstructor ) { - $.each( existingConstructor._childConstructors, function( i, child ) { - var childPrototype = child.prototype; + // Allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } - // redefine the child widget using the same prototype that was - // originally used, but inherit from the new version of the base - $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); - }); - // remove the list of existing child constructors from the old constructor - // so the old child constructors can be garbage collected - delete existingConstructor._childConstructors; - } else { - base._childConstructors.push( constructor ); - } + // Allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; - $.widget.bridge( name, constructor ); + // Extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, - return constructor; -}; + // Copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), -$.widget.extend = function( target ) { - var input = widget_slice.call( arguments, 1 ), - inputIndex = 0, - inputLength = input.length, - key, - value; - for ( ; inputIndex < inputLength; inputIndex++ ) { - for ( key in input[ inputIndex ] ) { - value = input[ inputIndex ][ key ]; - if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { - // Clone objects - if ( $.isPlainObject( value ) ) { - target[ key ] = $.isPlainObject( target[ key ] ) ? - $.widget.extend( {}, target[ key ], value ) : - // Don't extend strings, arrays, etc. with objects - $.widget.extend( {}, value ); - // Copy everything else by reference - } else { - target[ key ] = value; - } - } - } - } - return target; -}; + // Track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + } ); -$.widget.bridge = function( name, object ) { - var fullName = object.prototype.widgetFullName || name; - $.fn[ name ] = function( options ) { - var isMethodCall = typeof options === "string", - args = widget_slice.call( arguments, 1 ), - returnValue = this; + basePrototype = new base(); - if ( isMethodCall ) { - this.each(function() { - var methodValue, - instance = $.data( this, fullName ); - if ( options === "instance" ) { - returnValue = instance; - return false; - } - if ( !instance ) { - return $.error( "cannot call methods on " + name + " prior to initialization; " + - "attempted to call method '" + options + "'" ); - } - if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { - return $.error( "no such method '" + options + "' for " + name + " widget instance" ); - } - methodValue = instance[ options ].apply( instance, args ); - if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue && methodValue.jquery ? - returnValue.pushStack( methodValue.get() ) : - methodValue; - return false; - } - }); - } else { + // We need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = ( function() { + function _super() { + return base.prototype[ prop ].apply( this, arguments ); + } - // Allow multiple hashes to be passed on init - if ( args.length ) { - options = $.widget.extend.apply( null, [ options ].concat(args) ); - } + function _superApply( args ) { + return base.prototype[ prop ].apply( this, args ); + } - this.each(function() { - var instance = $.data( this, fullName ); - if ( instance ) { - instance.option( options || {} ); - if ( instance._init ) { - instance._init(); - } - } else { - $.data( this, fullName, new object( options, this ) ); - } - }); - } + return function() { + var __super = this._super; + var __superApply = this._superApply; + var returnValue; - return returnValue; - }; -}; + this._super = _super; + this._superApply = _superApply; -$.Widget = function( /* options, element */ ) {}; -$.Widget._childConstructors = []; + returnValue = value.apply( this, arguments ); -$.Widget.prototype = { - widgetName: "widget", - widgetEventPrefix: "", - defaultElement: "
", - options: { - disabled: false, + this._super = __super; + this._superApply = __superApply; - // callbacks - create: null - }, - _createWidget: function( options, element ) { - element = $( element || this.defaultElement || this )[ 0 ]; - this.element = $( element ); - this.uuid = widget_uuid++; - this.eventNamespace = "." + this.widgetName + this.uuid; + return returnValue; + }; + } )(); + } ); + constructor.prototype = $.widget.extend( basePrototype, { - this.bindings = $(); - this.hoverable = $(); - this.focusable = $(); + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + } ); - if ( element !== this ) { - $.data( element, this.widgetFullName, this ); - this._on( true, this.element, { - remove: function( event ) { - if ( event.target === element ) { - this.destroy(); - } - } - }); - this.document = $( element.style ? - // element within the document - element.ownerDocument : - // element is window or document - element.document || element ); - this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); - } + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; - this.options = $.widget.extend( {}, - this.options, - this._getCreateOptions(), - options ); + // Redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, + child._proto ); + } ); - this._create(); - this._trigger( "create", null, this._getCreateEventData() ); - this._init(); - }, - _getCreateOptions: $.noop, - _getCreateEventData: $.noop, - _create: $.noop, - _init: $.noop, + // Remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } - destroy: function() { - this._destroy(); - // we can probably remove the unbind calls in 2.0 - // all event bindings should go through this._on() - this.element - .unbind( this.eventNamespace ) - .removeData( this.widgetFullName ) - // support: jquery <1.6.3 - // http://bugs.jquery.com/ticket/9413 - .removeData( $.camelCase( this.widgetFullName ) ); - this.widget() - .unbind( this.eventNamespace ) - .removeAttr( "aria-disabled" ) - .removeClass( - this.widgetFullName + "-disabled " + - "ui-state-disabled" ); + $.widget.bridge( name, constructor ); - // clean up events and states - this.bindings.unbind( this.eventNamespace ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - }, - _destroy: $.noop, + return constructor; + }; - widget: function() { - return this.element; - }, + $.widget.extend = function( target ) { + var input = widgetSlice.call( arguments, 1 ); + var inputIndex = 0; + var inputLength = input.length; + var key; + var value; - option: function( key, value ) { - var options = key, - parts, - curOption, - i; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { - if ( arguments.length === 0 ) { - // don't return a reference to the internal hash - return $.widget.extend( {}, this.options ); - } + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : - if ( typeof key === "string" ) { - // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } - options = {}; - parts = key.split( "." ); - key = parts.shift(); - if ( parts.length ) { - curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); - for ( i = 0; i < parts.length - 1; i++ ) { - curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; - curOption = curOption[ parts[ i ] ]; - } - key = parts.pop(); - if ( arguments.length === 1 ) { - return curOption[ key ] === undefined ? null : curOption[ key ]; - } - curOption[ key ] = value; - } else { - if ( arguments.length === 1 ) { - return this.options[ key ] === undefined ? null : this.options[ key ]; - } - options[ key ] = value; - } - } + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); - this._setOptions( options ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; + }; - return this; - }, - _setOptions: function( options ) { - var key; + $.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string"; + var args = widgetSlice.call( arguments, 1 ); + var returnValue = this; - for ( key in options ) { - this._setOption( key, options[ key ] ); - } + if ( isMethodCall ) { - return this; - }, - _setOption: function( key, value ) { - this.options[ key ] = value; + // If this is an empty collection, we need to have the instance method + // return undefined instead of the jQuery instance + if ( !this.length && options === "instance" ) { + returnValue = undefined; + } else { + this.each( function() { + var methodValue; + var instance = $.data( this, fullName ); - if ( key === "disabled" ) { - this.widget() - .toggleClass( this.widgetFullName + "-disabled", !!value ); + if ( options === "instance" ) { + returnValue = instance; + return false; + } - // If the widget is becoming disabled, then nothing is interactive - if ( value ) { - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - } - } + if ( !instance ) { + return $.error( "cannot call methods on " + name + + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } - return this; - }, + if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + + " widget instance" ); + } - enable: function() { - return this._setOptions({ disabled: false }); - }, - disable: function() { - return this._setOptions({ disabled: true }); - }, + methodValue = instance[ options ].apply( instance, args ); - _on: function( suppressDisabledCheck, element, handlers ) { - var delegateElement, - instance = this; + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + } ); + } + } else { - // no suppressDisabledCheck flag, shuffle arguments - if ( typeof suppressDisabledCheck !== "boolean" ) { - handlers = element; - element = suppressDisabledCheck; - suppressDisabledCheck = false; - } + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat( args ) ); + } - // no element argument, shuffle and use this.element - if ( !handlers ) { - handlers = element; - element = this.element; - delegateElement = this.widget(); - } else { - element = delegateElement = $( element ); - this.bindings = this.bindings.add( element ); - } + this.each( function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + } ); + } - $.each( handlers, function( event, handler ) { - function handlerProxy() { - // allow widgets to customize the disabled handling - // - disabled as an array instead of boolean - // - disabled class as method for disabling individual parts - if ( !suppressDisabledCheck && - ( instance.options.disabled === true || - $( this ).hasClass( "ui-state-disabled" ) ) ) { - return; - } - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } + return returnValue; + }; + }; - // copy the guid so direct unbinding works - if ( typeof handler !== "string" ) { - handlerProxy.guid = handler.guid = - handler.guid || handlerProxy.guid || $.guid++; - } + $.Widget = function( /* options, element */ ) {}; + $.Widget._childConstructors = []; - var match = event.match( /^([\w:-]*)\s*(.*)$/ ), - eventName = match[1] + instance.eventNamespace, - selector = match[2]; - if ( selector ) { - delegateElement.delegate( selector, eventName, handlerProxy ); - } else { - element.bind( eventName, handlerProxy ); - } - }); - }, + $.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
", - _off: function( element, eventName ) { - eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + - this.eventNamespace; - element.unbind( eventName ).undelegate( eventName ); + options: { + classes: {}, + disabled: false, - // Clear the stack to avoid memory leaks (#10056) - this.bindings = $( this.bindings.not( element ).get() ); - this.focusable = $( this.focusable.not( element ).get() ); - this.hoverable = $( this.hoverable.not( element ).get() ); - }, + // Callbacks + create: null + }, - _delay: function( handler, delay ) { - function handlerProxy() { - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - var instance = this; - return setTimeout( handlerProxy, delay || 0 ); - }, + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = widgetUuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; - _hoverable: function( element ) { - this.hoverable = this.hoverable.add( element ); - this._on( element, { - mouseenter: function( event ) { - $( event.currentTarget ).addClass( "ui-state-hover" ); - }, - mouseleave: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-hover" ); - } - }); - }, + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + this.classesElementLookup = {}; - _focusable: function( element ) { - this.focusable = this.focusable.add( element ); - this._on( element, { - focusin: function( event ) { - $( event.currentTarget ).addClass( "ui-state-focus" ); - }, - focusout: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-focus" ); - } - }); - }, + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + } ); + this.document = $( element.style ? - _trigger: function( type, event, data ) { - var prop, orig, - callback = this.options[ type ]; + // Element within the document + element.ownerDocument : - data = data || {}; - event = $.Event( event ); - event.type = ( type === this.widgetEventPrefix ? - type : - this.widgetEventPrefix + type ).toLowerCase(); - // the original event may come from any element - // so we need to reset the target on the new event - event.target = this.element[ 0 ]; + // Element is window or document + element.document || element ); + this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow ); + } - // copy original event properties over to the new event - orig = event.originalEvent; - if ( orig ) { - for ( prop in orig ) { - if ( !( prop in event ) ) { - event[ prop ] = orig[ prop ]; - } - } - } + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); - this.element.trigger( event, data ); - return !( $.isFunction( callback ) && - callback.apply( this.element[0], [ event ].concat( data ) ) === false || - event.isDefaultPrevented() ); - } -}; + this._create(); -$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { - $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { - if ( typeof options === "string" ) { - options = { effect: options }; - } - var hasOptions, - effectName = !options ? - method : - options === true || typeof options === "number" ? - defaultEffect : - options.effect || defaultEffect; - options = options || {}; - if ( typeof options === "number" ) { - options = { duration: options }; - } - hasOptions = !$.isEmptyObject( options ); - options.complete = callback; - if ( options.delay ) { - element.delay( options.delay ); - } - if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { - element[ method ]( options ); - } else if ( effectName !== method && element[ effectName ] ) { - element[ effectName ]( options.duration, options.easing, callback ); - } else { - element.queue(function( next ) { - $( this )[ method ](); - if ( callback ) { - callback.call( element[ 0 ] ); - } - next(); - }); - } - }; -}); + if ( this.options.disabled ) { + this._setOptionDisabled( this.options.disabled ); + } + + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + + _getCreateOptions: function() { + return {}; + }, + + _getCreateEventData: $.noop, + + _create: $.noop, + + _init: $.noop, + + destroy: function() { + var that = this; + + this._destroy(); + $.each( this.classesElementLookup, function( key, value ) { + that._removeClass( value, key ); + } ); + + // We can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .off( this.eventNamespace ) + .removeData( this.widgetFullName ); + this.widget() + .off( this.eventNamespace ) + .removeAttr( "aria-disabled" ); + + // Clean up events and states + this.bindings.off( this.eventNamespace ); + }, + + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key; + var parts; + var curOption; + var i; + + if ( arguments.length === 0 ) { + + // Don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + + // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + + _setOption: function( key, value ) { + if ( key === "classes" ) { + this._setOptionClasses( value ); + } + + this.options[ key ] = value; + + if ( key === "disabled" ) { + this._setOptionDisabled( value ); + } + + return this; + }, + + _setOptionClasses: function( value ) { + var classKey, elements, currentElements; + + for ( classKey in value ) { + currentElements = this.classesElementLookup[ classKey ]; + if ( value[ classKey ] === this.options.classes[ classKey ] || + !currentElements || + !currentElements.length ) { + continue; + } + + // We are doing this to create a new jQuery object because the _removeClass() call + // on the next line is going to destroy the reference to the current elements being + // tracked. We need to save a copy of this collection so that we can add the new classes + // below. + elements = $( currentElements.get() ); + this._removeClass( currentElements, classKey ); + + // We don't use _addClass() here, because that uses this.options.classes + // for generating the string of classes. We want to use the value passed in from + // _setOption(), this is the new value of the classes option which was passed to + // _setOption(). We pass this value directly to _classes(). + elements.addClass( this._classes( { + element: elements, + keys: classKey, + classes: value, + add: true + } ) ); + } + }, + + _setOptionDisabled: function( value ) { + this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this._removeClass( this.hoverable, null, "ui-state-hover" ); + this._removeClass( this.focusable, null, "ui-state-focus" ); + } + }, + + enable: function() { + return this._setOptions( { disabled: false } ); + }, + + disable: function() { + return this._setOptions( { disabled: true } ); + }, + + _classes: function( options ) { + var full = []; + var that = this; + + options = $.extend( { + element: this.element, + classes: this.options.classes || {} + }, options ); + + function processClassString( classes, checkOption ) { + var current, i; + for ( i = 0; i < classes.length; i++ ) { + current = that.classesElementLookup[ classes[ i ] ] || $(); + if ( options.add ) { + current = $( $.unique( current.get().concat( options.element.get() ) ) ); + } else { + current = $( current.not( options.element ).get() ); + } + that.classesElementLookup[ classes[ i ] ] = current; + full.push( classes[ i ] ); + if ( checkOption && options.classes[ classes[ i ] ] ) { + full.push( options.classes[ classes[ i ] ] ); + } + } + } + + this._on( options.element, { + "remove": "_untrackClassesElement" + } ); + + if ( options.keys ) { + processClassString( options.keys.match( /\S+/g ) || [], true ); + } + if ( options.extra ) { + processClassString( options.extra.match( /\S+/g ) || [] ); + } + + return full.join( " " ); + }, + + _untrackClassesElement: function( event ) { + var that = this; + $.each( that.classesElementLookup, function( key, value ) { + if ( $.inArray( event.target, value ) !== -1 ) { + that.classesElementLookup[ key ] = $( value.not( event.target ).get() ); + } + } ); + }, + + _removeClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, false ); + }, + + _addClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, true ); + }, + + _toggleClass: function( element, keys, extra, add ) { + add = ( typeof add === "boolean" ) ? add : extra; + var shift = ( typeof element === "string" || element === null ), + options = { + extra: shift ? keys : extra, + keys: shift ? element : keys, + element: shift ? this.element : element, + add: add + }; + options.element.toggleClass( this._classes( options ), add ); + return this; + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement; + var instance = this; + + // No suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // No element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + + // Allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // Copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^([\w:-]*)\s*(.*)$/ ); + var eventName = match[ 1 ] + instance.eventNamespace; + var selector = match[ 2 ]; + + if ( selector ) { + delegateElement.on( eventName, selector, handlerProxy ); + } else { + element.on( eventName, handlerProxy ); + } + } ); + }, + + _off: function( element, eventName ) { + eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; + element.off( eventName ).off( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-hover" ); + }, + mouseleave: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-hover" ); + } + } ); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-focus" ); + }, + focusout: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-focus" ); + } + } ); + }, + + _trigger: function( type, event, data ) { + var prop, orig; + var callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + + // The original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // Copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } + }; + + $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + + var hasOptions; + var effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + + if ( options.delay ) { + element.delay( options.delay ); + } + + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue( function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + } ); + } + }; + } ); + + var widget = $.widget; -var widget = $.widget; diff --git a/public/file_uploader/package.json b/public/file_uploader/package.json deleted file mode 100755 index ed4d336..0000000 --- a/public/file_uploader/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "blueimp-file-upload", - "version": "9.18.0", - "title": "jQuery File Upload", - "description": "File Upload widget with multiple file selection, drag&drop support, progress bar, validation and preview images, audio and video for jQuery. Supports cross-domain, chunked and resumable file uploads. Works with any server-side platform (Google App Engine, PHP, Python, Ruby on Rails, Java, etc.) that supports standard HTML form file uploads.", - "keywords": [ - "jquery", - "file", - "upload", - "widget", - "multiple", - "selection", - "drag", - "drop", - "progress", - "preview", - "cross-domain", - "cross-site", - "chunk", - "resume", - "gae", - "go", - "python", - "php", - "bootstrap" - ], - "homepage": "https://github.com/blueimp/jQuery-File-Upload", - "author": { - "name": "Sebastian Tschan", - "url": "https://blueimp.net" - }, - "repository": { - "type": "git", - "url": "git://github.com/blueimp/jQuery-File-Upload.git" - }, - "license": "MIT", - "optionalDependencies": { - "blueimp-canvas-to-blob": "3.5.0", - "blueimp-load-image": "2.12.2", - "blueimp-tmpl": "3.6.0" - }, - "devDependencies": { - "bower-json": "0.8.1", - "jshint": "2.9.3" - }, - "scripts": { - "bower-version-update": "./bower-version-update.js", - "lint": "jshint *.js js/*.js js/cors/*.js", - "test": "npm run lint", - "preversion": "npm test", - "version": "npm run bower-version-update && git add bower.json", - "postversion": "git push --tags origin master && npm publish" - }, - "main": "js/jquery.fileupload.js" -} diff --git a/public/file_uploader/test/index.html b/public/file_uploader/test/index.html deleted file mode 100755 index 4a9a6f3..0000000 --- a/public/file_uploader/test/index.html +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - -jQuery File Upload Plugin Test - - - - -

jQuery File Upload Plugin Test

-

-
-

-
    -
    - -
    - -
    -
    - - - - Add files... - - - - - - - - -
    - -
    - -
    -
    -
    - -
     
    -
    -
    - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/file_uploader/test/test.js b/public/file_uploader/test/test.js deleted file mode 100755 index 4521275..0000000 --- a/public/file_uploader/test/test.js +++ /dev/null @@ -1,1292 +0,0 @@ -/* - * jQuery File Upload Plugin Test - * https://github.com/blueimp/jQuery-File-Upload - * - * Copyright 2010, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * https://opensource.org/licenses/MIT - */ - -/* global $, QUnit, window, document, expect, module, test, asyncTest, start, ok, strictEqual, notStrictEqual */ - -$(function () { - // jshint nomen:false - 'use strict'; - - QUnit.done = function () { - // Delete all uploaded files: - var url = $('#fileupload').prop('action'); - $.getJSON(url, function (result) { - $.each(result.files, function (index, file) { - $.ajax({ - url: url + '?file=' + encodeURIComponent(file.name), - type: 'DELETE' - }); - }); - }); - }; - - var lifecycle = { - setup: function () { - // Set the .fileupload method to the basic widget method: - $.widget('blueimp.fileupload', window.testBasicWidget, {}); - }, - teardown: function () { - // Remove all remaining event listeners: - $(document).unbind(); - } - }, - lifecycleUI = { - setup: function () { - // Set the .fileupload method to the UI widget method: - $.widget('blueimp.fileupload', window.testUIWidget, {}); - }, - teardown: function () { - // Remove all remaining event listeners: - $(document).unbind(); - } - }; - - module('Initialization', lifecycle); - - test('Widget initialization', function () { - var fu = $('#fileupload').fileupload(); - ok(fu.data('blueimp-fileupload') || fu.data('fileupload')); - }); - - test('Data attribute options', function () { - $('#fileupload').attr('data-url', 'http://example.org'); - $('#fileupload').fileupload(); - strictEqual( - $('#fileupload').fileupload('option', 'url'), - 'http://example.org' - ); - }); - - test('File input initialization', function () { - var fu = $('#fileupload').fileupload(); - ok( - fu.fileupload('option', 'fileInput').length, - 'File input field inside of the widget' - ); - ok( - fu.fileupload('option', 'fileInput').length, - 'Widget element as file input field' - ); - }); - - test('Drop zone initialization', function () { - ok($('#fileupload').fileupload() - .fileupload('option', 'dropZone').length); - }); - - test('Paste zone initialization', function () { - ok($('#fileupload').fileupload({pasteZone: document}) - .fileupload('option', 'pasteZone').length); - }); - - test('Event listeners initialization', function () { - expect( - $.support.xhrFormDataFileUpload ? 4 : 1 - ); - var eo = { - originalEvent: { - dataTransfer: {files: [{}], types: ['Files']}, - clipboardData: {items: [{}]} - } - }, - fu = $('#fileupload').fileupload({ - pasteZone: document, - dragover: function () { - ok(true, 'Triggers dragover callback'); - return false; - }, - drop: function () { - ok(true, 'Triggers drop callback'); - return false; - }, - paste: function () { - ok(true, 'Triggers paste callback'); - return false; - }, - change: function () { - ok(true, 'Triggers change callback'); - return false; - } - }), - fileInput = fu.fileupload('option', 'fileInput'), - dropZone = fu.fileupload('option', 'dropZone'), - pasteZone = fu.fileupload('option', 'pasteZone'); - fileInput.trigger($.Event('change', eo)); - dropZone.trigger($.Event('dragover', eo)); - dropZone.trigger($.Event('drop', eo)); - pasteZone.trigger($.Event('paste', eo)); - }); - - module('API', lifecycle); - - test('destroy', function () { - expect(4); - var eo = { - originalEvent: { - dataTransfer: {files: [{}], types: ['Files']}, - clipboardData: {items: [{}]} - } - }, - options = { - pasteZone: document, - dragover: function () { - ok(true, 'Triggers dragover callback'); - return false; - }, - drop: function () { - ok(true, 'Triggers drop callback'); - return false; - }, - paste: function () { - ok(true, 'Triggers paste callback'); - return false; - }, - change: function () { - ok(true, 'Triggers change callback'); - return false; - } - }, - fu = $('#fileupload').fileupload(options), - fileInput = fu.fileupload('option', 'fileInput'), - dropZone = fu.fileupload('option', 'dropZone'), - pasteZone = fu.fileupload('option', 'pasteZone'); - dropZone.bind('dragover', options.dragover); - dropZone.bind('drop', options.drop); - pasteZone.bind('paste', options.paste); - fileInput.bind('change', options.change); - fu.fileupload('destroy'); - fileInput.trigger($.Event('change', eo)); - dropZone.trigger($.Event('dragover', eo)); - dropZone.trigger($.Event('drop', eo)); - pasteZone.trigger($.Event('paste', eo)); - }); - - test('disable/enable', function () { - expect( - $.support.xhrFormDataFileUpload ? 4 : 1 - ); - var eo = { - originalEvent: { - dataTransfer: {files: [{}], types: ['Files']}, - clipboardData: {items: [{}]} - } - }, - fu = $('#fileupload').fileupload({ - pasteZone: document, - dragover: function () { - ok(true, 'Triggers dragover callback'); - return false; - }, - drop: function () { - ok(true, 'Triggers drop callback'); - return false; - }, - paste: function () { - ok(true, 'Triggers paste callback'); - return false; - }, - change: function () { - ok(true, 'Triggers change callback'); - return false; - } - }), - fileInput = fu.fileupload('option', 'fileInput'), - dropZone = fu.fileupload('option', 'dropZone'), - pasteZone = fu.fileupload('option', 'pasteZone'); - fu.fileupload('disable'); - fileInput.trigger($.Event('change', eo)); - dropZone.trigger($.Event('dragover', eo)); - dropZone.trigger($.Event('drop', eo)); - pasteZone.trigger($.Event('paste', eo)); - fu.fileupload('enable'); - fileInput.trigger($.Event('change', eo)); - dropZone.trigger($.Event('dragover', eo)); - dropZone.trigger($.Event('drop', eo)); - pasteZone.trigger($.Event('paste', eo)); - }); - - test('option', function () { - expect( - $.support.xhrFormDataFileUpload ? 10 : 7 - ); - var eo = { - originalEvent: { - dataTransfer: {files: [{}], types: ['Files']}, - clipboardData: {items: [{}]} - } - }, - fu = $('#fileupload').fileupload({ - pasteZone: document, - dragover: function () { - ok(true, 'Triggers dragover callback'); - return false; - }, - drop: function () { - ok(true, 'Triggers drop callback'); - return false; - }, - paste: function () { - ok(true, 'Triggers paste callback'); - return false; - }, - change: function () { - ok(true, 'Triggers change callback'); - return false; - } - }), - fileInput = fu.fileupload('option', 'fileInput'), - dropZone = fu.fileupload('option', 'dropZone'), - pasteZone = fu.fileupload('option', 'pasteZone'); - fu.fileupload('option', 'fileInput', null); - fu.fileupload('option', 'dropZone', null); - fu.fileupload('option', 'pasteZone', null); - fileInput.trigger($.Event('change', eo)); - dropZone.trigger($.Event('dragover', eo)); - dropZone.trigger($.Event('drop', eo)); - pasteZone.trigger($.Event('paste', eo)); - fu.fileupload('option', 'dropZone', 'body'); - strictEqual( - fu.fileupload('option', 'dropZone')[0], - document.body, - 'Allow a query string as parameter for the dropZone option' - ); - fu.fileupload('option', 'dropZone', document); - strictEqual( - fu.fileupload('option', 'dropZone')[0], - document, - 'Allow a document element as parameter for the dropZone option' - ); - fu.fileupload('option', 'pasteZone', 'body'); - strictEqual( - fu.fileupload('option', 'pasteZone')[0], - document.body, - 'Allow a query string as parameter for the pasteZone option' - ); - fu.fileupload('option', 'pasteZone', document); - strictEqual( - fu.fileupload('option', 'pasteZone')[0], - document, - 'Allow a document element as parameter for the pasteZone option' - ); - fu.fileupload('option', 'fileInput', ':file'); - strictEqual( - fu.fileupload('option', 'fileInput')[0], - $(':file')[0], - 'Allow a query string as parameter for the fileInput option' - ); - fu.fileupload('option', 'fileInput', $(':file')[0]); - strictEqual( - fu.fileupload('option', 'fileInput')[0], - $(':file')[0], - 'Allow a document element as parameter for the fileInput option' - ); - fu.fileupload('option', 'fileInput', fileInput); - fu.fileupload('option', 'dropZone', dropZone); - fu.fileupload('option', 'pasteZone', pasteZone); - fileInput.trigger($.Event('change', eo)); - dropZone.trigger($.Event('dragover', eo)); - dropZone.trigger($.Event('drop', eo)); - pasteZone.trigger($.Event('paste', eo)); - }); - - asyncTest('add', function () { - expect(2); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - add: function (e, data) { - strictEqual( - data.files[0].name, - param.files[0].name, - 'Triggers add callback' - ); - } - }).fileupload('add', param).fileupload( - 'option', - 'add', - function (e, data) { - data.submit().complete(function () { - ok(true, 'data.submit() Returns a jqXHR object'); - start(); - }); - } - ).fileupload('add', param); - }); - - asyncTest('send', function () { - expect(3); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - send: function (e, data) { - strictEqual( - data.files[0].name, - 'test', - 'Triggers send callback' - ); - } - }).fileupload('send', param).fail(function () { - ok(true, 'Allows to abort the request'); - }).complete(function () { - ok(true, 'Returns a jqXHR object'); - start(); - }).abort(); - }); - - module('Callbacks', lifecycle); - - asyncTest('add', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - add: function () { - ok(true, 'Triggers add callback'); - start(); - } - }).fileupload('add', param); - }); - - asyncTest('submit', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - submit: function () { - ok(true, 'Triggers submit callback'); - start(); - return false; - } - }).fileupload('add', param); - }); - - asyncTest('send', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - send: function () { - ok(true, 'Triggers send callback'); - start(); - return false; - } - }).fileupload('send', param); - }); - - asyncTest('done', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - done: function () { - ok(true, 'Triggers done callback'); - start(); - } - }).fileupload('send', param); - }); - - asyncTest('fail', function () { - expect(1); - var param = {files: [{name: 'test'}]}, - fu = $('#fileupload').fileupload({ - url: '404', - fail: function () { - ok(true, 'Triggers fail callback'); - start(); - } - }); - (fu.data('blueimp-fileupload') || fu.data('fileupload')) - ._isXHRUpload = function () { - return true; - }; - fu.fileupload('send', param); - }); - - asyncTest('always', function () { - expect(2); - var param = {files: [{name: 'test'}]}, - counter = 0, - fu = $('#fileupload').fileupload({ - always: function () { - ok(true, 'Triggers always callback'); - if (counter === 1) { - start(); - } else { - counter += 1; - } - } - }); - (fu.data('blueimp-fileupload') || fu.data('fileupload')) - ._isXHRUpload = function () { - return true; - }; - fu.fileupload('add', param).fileupload( - 'option', - 'url', - '404' - ).fileupload('add', param); - }); - - asyncTest('progress', function () { - expect(1); - var param = {files: [{name: 'test'}]}, - counter = 0; - $('#fileupload').fileupload({ - forceIframeTransport: true, - progress: function () { - ok(true, 'Triggers progress callback'); - if (counter === 0) { - start(); - } else { - counter += 1; - } - } - }).fileupload('send', param); - }); - - asyncTest('progressall', function () { - expect(1); - var param = {files: [{name: 'test'}]}, - counter = 0; - $('#fileupload').fileupload({ - forceIframeTransport: true, - progressall: function () { - ok(true, 'Triggers progressall callback'); - if (counter === 0) { - start(); - } else { - counter += 1; - } - } - }).fileupload('send', param); - }); - - asyncTest('start', function () { - expect(1); - var param = {files: [{name: '1'}, {name: '2'}]}, - active = 0; - $('#fileupload').fileupload({ - send: function () { - active += 1; - }, - start: function () { - ok(!active, 'Triggers start callback before uploads'); - start(); - } - }).fileupload('send', param); - }); - - asyncTest('stop', function () { - expect(1); - var param = {files: [{name: '1'}, {name: '2'}]}, - active = 0; - $('#fileupload').fileupload({ - send: function () { - active += 1; - }, - always: function () { - active -= 1; - }, - stop: function () { - ok(!active, 'Triggers stop callback after uploads'); - start(); - } - }).fileupload('send', param); - }); - - test('change', function () { - var fu = $('#fileupload').fileupload(), - fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'), - fileInput = fu.fileupload('option', 'fileInput'); - expect(2); - fu.fileupload({ - change: function (e, data) { - ok(true, 'Triggers change callback'); - strictEqual( - data.files.length, - 0, - 'Returns empty files list' - ); - }, - add: $.noop - }); - fuo._onChange({ - data: {fileupload: fuo}, - target: fileInput[0] - }); - }); - - test('paste', function () { - var fu = $('#fileupload').fileupload(), - fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'); - expect(1); - fu.fileupload({ - paste: function () { - ok(true, 'Triggers paste callback'); - }, - add: $.noop - }); - fuo._onPaste({ - data: {fileupload: fuo}, - originalEvent: { - dataTransfer: {files: [{}]}, - clipboardData: {items: [{}]} - }, - preventDefault: $.noop - }); - }); - - test('drop', function () { - var fu = $('#fileupload').fileupload(), - fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'); - expect(1); - fu.fileupload({ - drop: function () { - ok(true, 'Triggers drop callback'); - }, - add: $.noop - }); - fuo._onDrop({ - data: {fileupload: fuo}, - originalEvent: { - dataTransfer: {files: [{}]}, - clipboardData: {items: [{}]} - }, - preventDefault: $.noop - }); - }); - - test('dragover', function () { - var fu = $('#fileupload').fileupload(), - fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'); - expect(1); - fu.fileupload({ - dragover: function () { - ok(true, 'Triggers dragover callback'); - }, - add: $.noop - }); - fuo._onDragOver({ - data: {fileupload: fuo}, - originalEvent: {dataTransfer: {types: ['Files']}}, - preventDefault: $.noop - }); - }); - - module('Options', lifecycle); - - test('paramName', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - paramName: null, - send: function (e, data) { - strictEqual( - data.paramName[0], - data.fileInput.prop('name'), - 'Takes paramName from file input field if not set' - ); - return false; - } - }).fileupload('send', param); - }); - - test('url', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - url: null, - send: function (e, data) { - strictEqual( - data.url, - $(data.fileInput.prop('form')).prop('action'), - 'Takes url from form action if not set' - ); - return false; - } - }).fileupload('send', param); - }); - - test('type', function () { - expect(2); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - type: null, - send: function (e, data) { - strictEqual( - data.type, - 'POST', - 'Request type is "POST" if not set to "PUT"' - ); - return false; - } - }).fileupload('send', param); - $('#fileupload').fileupload({ - type: 'PUT', - send: function (e, data) { - strictEqual( - data.type, - 'PUT', - 'Request type is "PUT" if set to "PUT"' - ); - return false; - } - }).fileupload('send', param); - }); - - test('replaceFileInput', function () { - var fu = $('#fileupload').fileupload(), - fuo = fu.data('blueimp-fileupload') || fu.data('fileupload'), - fileInput = fu.fileupload('option', 'fileInput'), - fileInputElement = fileInput[0]; - expect(2); - fu.fileupload({ - replaceFileInput: false, - change: function () { - strictEqual( - fu.fileupload('option', 'fileInput')[0], - fileInputElement, - 'Keeps file input with replaceFileInput: false' - ); - }, - add: $.noop - }); - fuo._onChange({ - data: {fileupload: fuo}, - target: fileInput[0] - }); - fu.fileupload({ - replaceFileInput: true, - change: function () { - notStrictEqual( - fu.fileupload('option', 'fileInput')[0], - fileInputElement, - 'Replaces file input with replaceFileInput: true' - ); - }, - add: $.noop - }); - fuo._onChange({ - data: {fileupload: fuo}, - target: fileInput[0] - }); - }); - - asyncTest('forceIframeTransport', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - forceIframeTransport: true, - done: function (e, data) { - strictEqual( - data.dataType.substr(0, 6), - 'iframe', - 'Iframe Transport is used' - ); - start(); - } - }).fileupload('send', param); - }); - - test('singleFileUploads', function () { - expect(3); - var fu = $('#fileupload').fileupload(), - param = {files: [{name: '1'}, {name: '2'}]}, - index = 1; - (fu.data('blueimp-fileupload') || fu.data('fileupload')) - ._isXHRUpload = function () { - return true; - }; - $('#fileupload').fileupload({ - singleFileUploads: true, - add: function () { - ok(true, 'Triggers callback number ' + index.toString()); - index += 1; - } - }).fileupload('add', param).fileupload( - 'option', - 'singleFileUploads', - false - ).fileupload('add', param); - }); - - test('limitMultiFileUploads', function () { - expect(3); - var fu = $('#fileupload').fileupload(), - param = {files: [ - {name: '1'}, - {name: '2'}, - {name: '3'}, - {name: '4'}, - {name: '5'} - ]}, - index = 1; - (fu.data('blueimp-fileupload') || fu.data('fileupload')) - ._isXHRUpload = function () { - return true; - }; - $('#fileupload').fileupload({ - singleFileUploads: false, - limitMultiFileUploads: 2, - add: function () { - ok(true, 'Triggers callback number ' + index.toString()); - index += 1; - } - }).fileupload('add', param); - }); - - test('limitMultiFileUploadSize', function () { - expect(7); - var fu = $('#fileupload').fileupload(), - param = {files: [ - {name: '1-1', size: 100000}, - {name: '1-2', size: 40000}, - {name: '2-1', size: 100000}, - {name: '3-1', size: 50000}, - {name: '3-2', size: 40000}, - {name: '4-1', size: 45000} // New request due to limitMultiFileUploads - ]}, - param2 = {files: [ - {name: '5-1'}, - {name: '5-2'}, - {name: '6-1'}, - {name: '6-2'}, - {name: '7-1'} - ]}, - index = 1; - (fu.data('blueimp-fileupload') || fu.data('fileupload')) - ._isXHRUpload = function () { - return true; - }; - $('#fileupload').fileupload({ - singleFileUploads: false, - limitMultiFileUploads: 2, - limitMultiFileUploadSize: 150000, - limitMultiFileUploadSizeOverhead: 5000, - add: function () { - ok(true, 'Triggers callback number ' + index.toString()); - index += 1; - } - }).fileupload('add', param).fileupload('add', param2); - }); - - asyncTest('sequentialUploads', function () { - expect(6); - var param = {files: [ - {name: '1'}, - {name: '2'}, - {name: '3'}, - {name: '4'}, - {name: '5'}, - {name: '6'} - ]}, - addIndex = 0, - sendIndex = 0, - loadIndex = 0, - fu = $('#fileupload').fileupload({ - sequentialUploads: true, - add: function (e, data) { - addIndex += 1; - if (addIndex === 4) { - data.submit().abort(); - } else { - data.submit(); - } - }, - send: function () { - sendIndex += 1; - }, - done: function () { - loadIndex += 1; - strictEqual(sendIndex, loadIndex, 'upload in order'); - }, - fail: function (e, data) { - strictEqual(data.errorThrown, 'abort', 'upload aborted'); - }, - stop: function () { - start(); - } - }); - (fu.data('blueimp-fileupload') || fu.data('fileupload')) - ._isXHRUpload = function () { - return true; - }; - fu.fileupload('add', param); - }); - - asyncTest('limitConcurrentUploads', function () { - expect(12); - var param = {files: [ - {name: '1'}, - {name: '2'}, - {name: '3'}, - {name: '4'}, - {name: '5'}, - {name: '6'}, - {name: '7'}, - {name: '8'}, - {name: '9'}, - {name: '10'}, - {name: '11'}, - {name: '12'} - ]}, - addIndex = 0, - sendIndex = 0, - loadIndex = 0, - fu = $('#fileupload').fileupload({ - limitConcurrentUploads: 3, - add: function (e, data) { - addIndex += 1; - if (addIndex === 4) { - data.submit().abort(); - } else { - data.submit(); - } - }, - send: function () { - sendIndex += 1; - }, - done: function () { - loadIndex += 1; - ok(sendIndex - loadIndex < 3); - }, - fail: function (e, data) { - strictEqual(data.errorThrown, 'abort', 'upload aborted'); - }, - stop: function () { - start(); - } - }); - (fu.data('blueimp-fileupload') || fu.data('fileupload')) - ._isXHRUpload = function () { - return true; - }; - fu.fileupload('add', param); - }); - - if ($.support.xhrFileUpload) { - asyncTest('multipart', function () { - expect(2); - var param = {files: [{ - name: 'test.png', - size: 123, - type: 'image/png' - }]}, - fu = $('#fileupload').fileupload({ - multipart: false, - always: function (e, data) { - strictEqual( - data.contentType, - param.files[0].type, - 'non-multipart upload sets file type as contentType' - ); - strictEqual( - data.headers['Content-Disposition'], - 'attachment; filename="' + param.files[0].name + '"', - 'non-multipart upload sets Content-Disposition header' - ); - start(); - } - }); - fu.fileupload('send', param); - }); - } - - module('UI Initialization', lifecycleUI); - - test('Widget initialization', function () { - var fu = $('#fileupload').fileupload(); - ok(fu.data('blueimp-fileupload') || fu.data('fileupload')); - ok( - $('#fileupload').fileupload('option', 'uploadTemplate').length, - 'Initialized upload template' - ); - ok( - $('#fileupload').fileupload('option', 'downloadTemplate').length, - 'Initialized download template' - ); - }); - - test('Buttonbar event listeners', function () { - var buttonbar = $('#fileupload .fileupload-buttonbar'), - files = [{name: 'test'}]; - expect(4); - $('#fileupload').fileupload({ - send: function () { - ok(true, 'Started file upload via global start button'); - }, - fail: function (e, data) { - ok(true, 'Canceled file upload via global cancel button'); - data.context.remove(); - }, - destroy: function () { - ok(true, 'Delete action called via global delete button'); - } - }); - $('#fileupload').fileupload('add', {files: files}); - buttonbar.find('.cancel').click(); - $('#fileupload').fileupload('add', {files: files}); - buttonbar.find('.start').click(); - buttonbar.find('.cancel').click(); - files[0].deleteUrl = 'http://example.org/banana.jpg'; - ($('#fileupload').data('blueimp-fileupload') || - $('#fileupload').data('fileupload')) - ._renderDownload(files) - .appendTo($('#fileupload .files')).show() - .find('.toggle').click(); - buttonbar.find('.delete').click(); - }); - - module('UI API', lifecycleUI); - - test('destroy', function () { - var buttonbar = $('#fileupload .fileupload-buttonbar'), - files = [{name: 'test'}]; - expect(1); - $('#fileupload').fileupload({ - send: function () { - ok(true, 'This test should not run'); - return false; - } - }) - .fileupload('add', {files: files}) - .fileupload('destroy'); - buttonbar.find('.start').click(function () { - ok(true, 'Clicked global start button'); - return false; - }).click(); - }); - - test('disable/enable', function () { - var buttonbar = $('#fileupload .fileupload-buttonbar'); - $('#fileupload').fileupload(); - $('#fileupload').fileupload('disable'); - strictEqual( - buttonbar.find('input[type=file], button').not(':disabled').length, - 0, - 'Disables the buttonbar buttons' - ); - $('#fileupload').fileupload('enable'); - strictEqual( - buttonbar.find('input[type=file], button').not(':disabled').length, - 4, - 'Enables the buttonbar buttons' - ); - }); - - module('UI Callbacks', lifecycleUI); - - test('destroy', function () { - expect(3); - $('#fileupload').fileupload({ - destroy: function (e, data) { - ok(true, 'Triggers destroy callback'); - strictEqual( - data.url, - 'test', - 'Passes over deletion url parameter' - ); - strictEqual( - data.type, - 'DELETE', - 'Passes over deletion request type parameter' - ); - } - }); - ($('#fileupload').data('blueimp-fileupload') || - $('#fileupload').data('fileupload')) - ._renderDownload([{ - name: 'test', - deleteUrl: 'test', - deleteType: 'DELETE' - }]) - .appendTo($('#fileupload .files')) - .show() - .find('.toggle').click(); - $('#fileupload .fileupload-buttonbar .delete').click(); - }); - - asyncTest('added', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - added: function (e, data) { - start(); - strictEqual( - data.files[0].name, - param.files[0].name, - 'Triggers added callback' - ); - }, - send: function () { - return false; - } - }).fileupload('add', param); - }); - - asyncTest('started', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - started: function () { - start(); - ok('Triggers started callback'); - return false; - }, - sent: function () { - return false; - } - }).fileupload('send', param); - }); - - asyncTest('sent', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - sent: function (e, data) { - start(); - strictEqual( - data.files[0].name, - param.files[0].name, - 'Triggers sent callback' - ); - return false; - } - }).fileupload('send', param); - }); - - asyncTest('completed', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - completed: function () { - start(); - ok('Triggers completed callback'); - return false; - } - }).fileupload('send', param); - }); - - asyncTest('failed', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - failed: function () { - start(); - ok('Triggers failed callback'); - return false; - } - }).fileupload('send', param).abort(); - }); - - asyncTest('stopped', function () { - expect(1); - var param = {files: [{name: 'test'}]}; - $('#fileupload').fileupload({ - stopped: function () { - start(); - ok('Triggers stopped callback'); - return false; - } - }).fileupload('send', param); - }); - - asyncTest('destroyed', function () { - expect(1); - $('#fileupload').fileupload({ - dataType: 'html', - destroyed: function () { - start(); - ok(true, 'Triggers destroyed callback'); - } - }); - ($('#fileupload').data('blueimp-fileupload') || - $('#fileupload').data('fileupload')) - ._renderDownload([{ - name: 'test', - deleteUrl: '.', - deleteType: 'GET' - }]) - .appendTo($('#fileupload .files')) - .show() - .find('.toggle').click(); - $('#fileupload .fileupload-buttonbar .delete').click(); - }); - - module('UI Options', lifecycleUI); - - test('autoUpload', function () { - expect(1); - $('#fileupload') - .fileupload({ - autoUpload: true, - send: function () { - ok(true, 'Started file upload automatically'); - return false; - } - }) - .fileupload('add', {files: [{name: 'test'}]}) - .fileupload('option', 'autoUpload', false) - .fileupload('add', {files: [{name: 'test'}]}); - }); - - test('maxNumberOfFiles', function () { - expect(3); - var addIndex = 0, - sendIndex = 0; - $('#fileupload') - .fileupload({ - autoUpload: true, - maxNumberOfFiles: 3, - singleFileUploads: false, - send: function () { - strictEqual( - sendIndex += 1, - addIndex - ); - }, - progress: $.noop, - progressall: $.noop, - done: $.noop, - stop: $.noop - }) - .fileupload('add', {files: [{name: (addIndex += 1)}]}) - .fileupload('add', {files: [{name: (addIndex += 1)}]}) - .fileupload('add', {files: [{name: (addIndex += 1)}]}) - .fileupload('add', {files: [{name: 'test'}]}); - }); - - test('maxFileSize', function () { - expect(2); - var addIndex = 0, - sendIndex = 0; - $('#fileupload') - .fileupload({ - autoUpload: true, - maxFileSize: 1000, - send: function () { - strictEqual( - sendIndex += 1, - addIndex - ); - return false; - } - }) - .fileupload('add', {files: [{ - name: (addIndex += 1) - }]}) - .fileupload('add', {files: [{ - name: (addIndex += 1), - size: 999 - }]}) - .fileupload('add', {files: [{ - name: 'test', - size: 1001 - }]}) - .fileupload({ - send: function (e, data) { - ok( - !$.blueimp.fileupload.prototype.options - .send.call(this, e, data) - ); - return false; - } - }); - }); - - test('minFileSize', function () { - expect(2); - var addIndex = 0, - sendIndex = 0; - $('#fileupload') - .fileupload({ - autoUpload: true, - minFileSize: 1000, - send: function () { - strictEqual( - sendIndex += 1, - addIndex - ); - return false; - } - }) - .fileupload('add', {files: [{ - name: (addIndex += 1) - }]}) - .fileupload('add', {files: [{ - name: (addIndex += 1), - size: 1001 - }]}) - .fileupload('add', {files: [{ - name: 'test', - size: 999 - }]}) - .fileupload({ - send: function (e, data) { - ok( - !$.blueimp.fileupload.prototype.options - .send.call(this, e, data) - ); - return false; - } - }); - }); - - test('acceptFileTypes', function () { - expect(2); - var addIndex = 0, - sendIndex = 0; - $('#fileupload') - .fileupload({ - autoUpload: true, - acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, - disableImageMetaDataLoad: true, - send: function () { - strictEqual( - sendIndex += 1, - addIndex - ); - return false; - } - }) - .fileupload('add', {files: [{ - name: (addIndex += 1) + '.jpg' - }]}) - .fileupload('add', {files: [{ - name: (addIndex += 1), - type: 'image/jpeg' - }]}) - .fileupload('add', {files: [{ - name: 'test.txt', - type: 'text/plain' - }]}) - .fileupload({ - send: function (e, data) { - ok( - !$.blueimp.fileupload.prototype.options - .send.call(this, e, data) - ); - return false; - } - }); - }); - - test('acceptFileTypes as HTML5 data attribute', function () { - expect(2); - var regExp = /(\.|\/)(gif|jpe?g|png)$/i; - $('#fileupload') - .attr('data-accept-file-types', regExp.toString()) - .fileupload(); - strictEqual( - $.type($('#fileupload').fileupload('option', 'acceptFileTypes')), - $.type(regExp) - ); - strictEqual( - $('#fileupload').fileupload('option', 'acceptFileTypes').toString(), - regExp.toString() - ); - }); - -}); From 35e03f7bad94666c225f4640a447e4a4cc2a3cb0 Mon Sep 17 00:00:00 2001 From: Denis Fedoseev Date: Tue, 31 Jul 2018 09:17:32 +0300 Subject: [PATCH 4/9] image processing was moved to sub --- fotostore.pl | 125 ++++++++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 56 deletions(-) diff --git a/fotostore.pl b/fotostore.pl index 7f09911..fd6bfd5 100755 --- a/fotostore.pl +++ b/fotostore.pl @@ -44,6 +44,8 @@ my $sha = Digest::SHA->new('sha256'); # Directory to save image files my $IMAGE_DIR = File::Spec->catfile( getcwd(), 'public', $IMAGE_BASE ); +my $log = Mojo::Log->new(); + plugin 'authentication', { autoload_user => 1, load_user => sub { @@ -93,10 +95,10 @@ get '/register' => ( authenticated => 0 ) => sub { post '/register' => ( authenticated => 0 ) => sub { my $self = shift; - my $username = $self->req->param('username'); - my $password = $self->req->param('password'); - my $fullname = $self->req->param('fullname'); - my $invite = $self->req->param('invite'); + my $username = $self->req->param('username') || ""; + my $password = $self->req->param('password') || ""; + my $fullname = $self->req->param('fullname') || ""; + my $invite = $self->req->param('invite') || ""; if ( $invite eq $config->{'invite_code'} ) { @@ -240,58 +242,7 @@ post '/upload' => ( authenticated => 1 ) => sub { ); } - # Extention - my $exts = { - 'image/gif' => 'gif', - 'image/jpeg' => 'jpg', - 'image/png' => 'png' - }; - my $ext = $exts->{$image_type}; - - # Image file - my $filename = sprintf( '%s.%s', create_hash( $image->slurp() ), $ext ); - my $image_file = - File::Spec->catfile( get_path( $user_id, $ORIG_DIR ), $filename ); - - # Save to file - $image->move_to($image_file); - - my $imager = Imager->new(); - $imager->read( file => $image_file ) or die $imager->errstr; - - #http://sylvana.net/jpegcrop/exif_orientation.html - #http://myjaphoo.de/docs/exifidentifiers.html - my $rotation_angle = $imager->tags( name => "exif_orientation" ) || 1; - $self->app->log->info( - "Rotation angle [" . $rotation_angle . "] [" . $image->filename . "]" ); - - if ( $rotation_angle == 3 ) { - $imager = $imager->rotate( degrees => 180 ); - } - elsif ( $rotation_angle == 6 ) { - $imager = $imager->rotate( degrees => 90 ); - } - - my $original_width = $imager->getwidth(); - - for my $scale (@scale_width) { - - #Skip sizes which more than original image - if ( $scale >= $original_width ) { - next; - } - - my $scaled = $imager->scale( xpixels => $scale ); - - $scaled->write( file => - File::Spec->catfile( get_path( $user_id, $scale ), $filename ) ) - or die $scaled->errstr; - } - - if ( !$db->add_file( $user->{'user_id'}, $filename, $image->filename ) ) { - - #TODO: Send error msg - } + my $filename = store_image($image, $user_id); $self->render( json => { @@ -327,4 +278,66 @@ sub get_path { return $path; } +sub store_image { + my $image = shift; + my $user_id = shift; + + my $image_type = $image->headers->content_type; + + # Extention + my $exts = { + 'image/gif' => 'gif', + 'image/jpeg' => 'jpg', + 'image/png' => 'png' + }; + my $ext = $exts->{$image_type}; + + # Image file + my $filename = sprintf( '%s.%s', create_hash( $image->slurp() ), $ext ); + my $image_file = + File::Spec->catfile( get_path( $user_id, $ORIG_DIR ), $filename ); + + # Save to file + $image->move_to($image_file); + + my $imager = Imager->new(); + $imager->read( file => $image_file ) or die $imager->errstr; + + #http://sylvana.net/jpegcrop/exif_orientation.html + #http://myjaphoo.de/docs/exifidentifiers.html + my $rotation_angle = $imager->tags( name => "exif_orientation" ) || 1; + $log->debug( + "Rotation angle [" . $rotation_angle . "] [" . $image->filename . "]" ); + + if ( $rotation_angle == 3 ) { + $imager = $imager->rotate( degrees => 180 ); + } + elsif ( $rotation_angle == 6 ) { + $imager = $imager->rotate( degrees => 90 ); + } + + my $original_width = $imager->getwidth(); + + for my $scale (@scale_width) { + + #Skip sizes which more than original image + if ( $scale >= $original_width ) { + next; + } + + my $scaled = $imager->scale( xpixels => $scale ); + + $scaled->write( file => + File::Spec->catfile( get_path( $user_id, $scale ), $filename ) ) + or die $scaled->errstr; + } + + if ( !$db->add_file( $user_id, $filename, $image->filename ) ) { + + die sprintf('Can\'t save file %s', $filename); + } + + return $filename; +} + app->start; From a774698d6e055ee131622468dc73141b912b3c66 Mon Sep 17 00:00:00 2001 From: Denis Fedoseev Date: Wed, 1 Aug 2018 09:14:41 +0300 Subject: [PATCH 5/9] process image in subprocess --- fotostore.pl | 61 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/fotostore.pl b/fotostore.pl index fd6bfd5..a56b121 100755 --- a/fotostore.pl +++ b/fotostore.pl @@ -5,7 +5,7 @@ use warnings; use lib 'lib'; use Mojolicious::Lite; # app, get, post is exported. -use File::Basename 'basename'; +use File::Basename qw/basename fileparse/; use File::Path 'mkpath'; use File::Spec 'catfile'; use Cwd; @@ -220,7 +220,6 @@ post '/upload' => ( authenticated => 1 ) => sub { my $user = $self->current_user(); my $user_id = $user->{'user_id'}; - # $self->app->log->debug( "user:" . Dumper($user) ); # Not upload unless ($image) { @@ -232,7 +231,11 @@ post '/upload' => ( authenticated => 1 ) => sub { # Check file type my $image_type = $image->headers->content_type; - my %valid_types = map { $_ => 1 } qw(image/gif image/jpeg image/png); + my %valid_types = ( + 'image/gif' => 'gif', + 'image/jpeg' => 'jpg', + 'image/png' => 'png' + ); # Content type is wrong unless ( $valid_types{$image_type} ) { @@ -242,7 +245,31 @@ post '/upload' => ( authenticated => 1 ) => sub { ); } - my $filename = store_image($image, $user_id); + my $ext = $valid_types{$image_type}; + + # Image file + my $filename = sprintf( '%s.%s', create_hash( $image->slurp() ), $ext ); + my $image_file = + File::Spec->catfile( get_path( $user_id, $ORIG_DIR ), $filename ); + + # Save to file + $image->move_to($image_file); + + # Operation that would block the event loop for 5 seconds + my $subprocess = Mojo::IOLoop::Subprocess->new; + $subprocess->run( + sub { + my $subprocess = shift; + store_image($image_file, $image->filename, $user_id); + }, + sub { + my ($subprocess, $err, @results) = @_; + say "Subprocess error: $err" and return if $err; + say "I $results[0] $results[1]!"; + } + ); + + $subprocess->ioloop->start unless $subprocess->ioloop->is_running; $self->render( json => { @@ -279,27 +306,12 @@ sub get_path { } sub store_image { - my $image = shift; + my $image_file = shift; + my $original_filename = shift; my $user_id = shift; - my $image_type = $image->headers->content_type; - - # Extention - my $exts = { - 'image/gif' => 'gif', - 'image/jpeg' => 'jpg', - 'image/png' => 'png' - }; - my $ext = $exts->{$image_type}; - - # Image file - my $filename = sprintf( '%s.%s', create_hash( $image->slurp() ), $ext ); - my $image_file = - File::Spec->catfile( get_path( $user_id, $ORIG_DIR ), $filename ); - - # Save to file - $image->move_to($image_file); + my $filename = fileparse($image_file); my $imager = Imager->new(); $imager->read( file => $image_file ) or die $imager->errstr; @@ -307,7 +319,7 @@ sub store_image { #http://myjaphoo.de/docs/exifidentifiers.html my $rotation_angle = $imager->tags( name => "exif_orientation" ) || 1; $log->debug( - "Rotation angle [" . $rotation_angle . "] [" . $image->filename . "]" ); + "Rotation angle [" . $rotation_angle . "]" ); if ( $rotation_angle == 3 ) { $imager = $imager->rotate( degrees => 180 ); @@ -332,8 +344,9 @@ sub store_image { or die $scaled->errstr; } - if ( !$db->add_file( $user_id, $filename, $image->filename ) ) { + if ( !$db->add_file( $user_id, $filename, $original_filename ) ) { + $log->error(sprintf('Can\'t save file %s', $filename)); die sprintf('Can\'t save file %s', $filename); } From 87ad4111a8cbcdd968d6b0feb51efb27d5f709ec Mon Sep 17 00:00:00 2001 From: Denis Fedoseev Date: Wed, 1 Aug 2018 10:11:43 +0300 Subject: [PATCH 6/9] spawn subprocess with promise --- fotostore.pl | 152 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 93 insertions(+), 59 deletions(-) diff --git a/fotostore.pl b/fotostore.pl index a56b121..6884b13 100755 --- a/fotostore.pl +++ b/fotostore.pl @@ -4,6 +4,8 @@ use warnings; use lib 'lib'; use Mojolicious::Lite; # app, get, post is exported. +use Mojo::Promise; +use Mojo::IOLoop; use File::Basename qw/basename fileparse/; use File::Path 'mkpath'; @@ -255,34 +257,48 @@ post '/upload' => ( authenticated => 1 ) => sub { # Save to file $image->move_to($image_file); - # Operation that would block the event loop for 5 seconds - my $subprocess = Mojo::IOLoop::Subprocess->new; - $subprocess->run( - sub { - my $subprocess = shift; - store_image($image_file, $image->filename, $user_id); - }, - sub { - my ($subprocess, $err, @results) = @_; - say "Subprocess error: $err" and return if $err; - say "I $results[0] $results[1]!"; - } - ); + $log->debug("Spwan subprocess"); + + my $promise = store_image($image_file, $image->filename, $user_id); + - $subprocess->ioloop->start unless $subprocess->ioloop->is_running; - - $self->render( - json => { - files => [ - { - name => $image->filename, - size => $image->size, - url => sprintf( '/images/orig/%s', $filename ), - thumbnailUrl => sprintf( '/images/200/%s', $filename ), + Mojo::Promise->all($promise)->then(sub { + $self->render( + json => { + files => [ + { + name => $image->filename, + size => $image->size, + url => sprintf( '/images/orig/%s', $filename ), + thumbnailUrl => sprintf( '/images/200/%s', $filename ), + } + ] } - ] - } - ); + ); + })->wait; + + + # $log->debug("wait for promise"); + # Mojo::Promise->all($spawn_subprocess)->then(sub { + # $self->render( + # json => { + # files => [ + # { + # name => $image->filename, + # size => $image->size, + # url => sprintf( '/images/orig/%s', $filename ), + # thumbnailUrl => sprintf( '/images/200/%s', $filename ), + # } + # ] + # } + # ); + + # })->catch(sub { + # my $err = shift; + # warn "Something went wrong: $err"; + # })->wait; + + # Redirect to top page # $self->redirect_to('index'); @@ -310,47 +326,65 @@ sub store_image { my $original_filename = shift; my $user_id = shift; + my $promise = Mojo::Promise->new; + # Operation that would block the event loop for 5 seconds + Mojo::IOLoop->subprocess( + sub { + my $subprocess = shift; - my $filename = fileparse($image_file); - my $imager = Imager->new(); - $imager->read( file => $image_file ) or die $imager->errstr; + my $filename = fileparse($image_file); + my $imager = Imager->new(); + $imager->read( file => $image_file ) or die $imager->errstr; - #http://sylvana.net/jpegcrop/exif_orientation.html - #http://myjaphoo.de/docs/exifidentifiers.html - my $rotation_angle = $imager->tags( name => "exif_orientation" ) || 1; - $log->debug( - "Rotation angle [" . $rotation_angle . "]" ); + #http://sylvana.net/jpegcrop/exif_orientation.html + #http://myjaphoo.de/docs/exifidentifiers.html + my $rotation_angle = $imager->tags( name => "exif_orientation" ) || 1; + $log->debug( + "Rotation angle [" . $rotation_angle . "]" ); - if ( $rotation_angle == 3 ) { - $imager = $imager->rotate( degrees => 180 ); - } - elsif ( $rotation_angle == 6 ) { - $imager = $imager->rotate( degrees => 90 ); - } + if ( $rotation_angle == 3 ) { + $imager = $imager->rotate( degrees => 180 ); + } + elsif ( $rotation_angle == 6 ) { + $imager = $imager->rotate( degrees => 90 ); + } - my $original_width = $imager->getwidth(); + my $original_width = $imager->getwidth(); - for my $scale (@scale_width) { + for my $scale (@scale_width) { - #Skip sizes which more than original image - if ( $scale >= $original_width ) { - next; + #Skip sizes which more than original image + if ( $scale >= $original_width ) { + next; + } + + my $scaled = $imager->scale( xpixels => $scale ); + + $scaled->write( file => + File::Spec->catfile( get_path( $user_id, $scale ), $filename ) ) + or die $scaled->errstr; + } + + if ( !$db->add_file( $user_id, $filename, $original_filename ) ) { + + $log->error(sprintf('Can\'t save file %s', $filename)); + die sprintf('Can\'t save file %s', $filename); + } + + $log->debug("done!"); + return $filename; + }, + sub { + my ($subprocess, $err, @results) = @_; + say "Subprocess error: $err" and return if $err; + $promise->reject("I $results[0] $results[1]!") if $err; + $promise->resolve(@results); } + ); - my $scaled = $imager->scale( xpixels => $scale ); - - $scaled->write( file => - File::Spec->catfile( get_path( $user_id, $scale ), $filename ) ) - or die $scaled->errstr; - } - - if ( !$db->add_file( $user_id, $filename, $original_filename ) ) { - - $log->error(sprintf('Can\'t save file %s', $filename)); - die sprintf('Can\'t save file %s', $filename); - } - - return $filename; + return $promise; + } +Mojo::IOLoop->start; app->start; From d4a1f99b7148538b6a7f95f9d8f1c1158f093389 Mon Sep 17 00:00:00 2001 From: Denis Fedoseev Date: Wed, 1 Aug 2018 10:18:24 +0300 Subject: [PATCH 7/9] small cleanup --- fotostore.pl | 39 +++++---------------------------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/fotostore.pl b/fotostore.pl index 6884b13..96da165 100755 --- a/fotostore.pl +++ b/fotostore.pl @@ -257,11 +257,10 @@ post '/upload' => ( authenticated => 1 ) => sub { # Save to file $image->move_to($image_file); - $log->debug("Spwan subprocess"); my $promise = store_image($image_file, $image->filename, $user_id); - + #TODO: add errors handling Mojo::Promise->all($promise)->then(sub { $self->render( json => { @@ -277,32 +276,6 @@ post '/upload' => ( authenticated => 1 ) => sub { ); })->wait; - - # $log->debug("wait for promise"); - # Mojo::Promise->all($spawn_subprocess)->then(sub { - # $self->render( - # json => { - # files => [ - # { - # name => $image->filename, - # size => $image->size, - # url => sprintf( '/images/orig/%s', $filename ), - # thumbnailUrl => sprintf( '/images/200/%s', $filename ), - # } - # ] - # } - # ); - - # })->catch(sub { - # my $err = shift; - # warn "Something went wrong: $err"; - # })->wait; - - - - # Redirect to top page - # $self->redirect_to('index'); - } => 'upload'; sub create_hash { @@ -327,7 +300,7 @@ sub store_image { my $user_id = shift; my $promise = Mojo::Promise->new; - # Operation that would block the event loop for 5 seconds + # Process and store uploaded file in a separate process Mojo::IOLoop->subprocess( sub { my $subprocess = shift; @@ -371,19 +344,17 @@ sub store_image { die sprintf('Can\'t save file %s', $filename); } - $log->debug("done!"); return $filename; }, sub { my ($subprocess, $err, @results) = @_; - say "Subprocess error: $err" and return if $err; - $promise->reject("I $results[0] $results[1]!") if $err; - $promise->resolve(@results); + $log->error("Subprocess error: $err") and return if $err; + $promise->reject("Subprocess error: $err @results") if $err; + $promise->resolve(1, @results); } ); return $promise; - } Mojo::IOLoop->start; From 7eaa376126a7337218008981b545612b3a1302de Mon Sep 17 00:00:00 2001 From: Denis Fedoseev Date: Fri, 3 Aug 2018 11:00:52 +0300 Subject: [PATCH 8/9] mojo version --- cpanfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpanfile b/cpanfile index 265bb40..93b7cf7 100644 --- a/cpanfile +++ b/cpanfile @@ -1,4 +1,4 @@ -requires 'Mojolicious'; +requires 'Mojolicious', '>= 7.88'; requires 'Mojolicious::Plugin::Authentication'; requires 'Imager'; requires 'File::Basename'; From 156274c1097e806f04c3490b4ea53821bc8f005e Mon Sep 17 00:00:00 2001 From: Denis Fedoseev Date: Fri, 3 Aug 2018 23:19:35 +0300 Subject: [PATCH 9/9] upload progress and icons were fixed --- public/css/main.css | 18 ++++++++++++++++++ public/img/copy_icon.png | Bin 2637 -> 600 bytes public/img/more_icon.png | Bin 6793 -> 416 bytes templates/includes/images_list.html.ep | 6 +++--- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/public/css/main.css b/public/css/main.css index 7a218c2..17b16a1 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -15,6 +15,15 @@ padding: 5px; } +.copy-img:before { + content: url(/img/copy_icon.png); + width: 32px; + height: 32px; + overflow: hidden; + cursor: pointer; + position: relative; +} + .copy-img { content: url(/img/copy_icon.png); width: 32px; @@ -24,6 +33,15 @@ position: relative; } +.copy-bb-more:before { + content: url(/img/more_icon.png); + width: 32px; + height: 32px; + overflow: hidden; + cursor: pointer; + position: relative; +} + .copy-bb-more { content: url(/img/more_icon.png); width: 32px; diff --git a/public/img/copy_icon.png b/public/img/copy_icon.png index d7a64e00e8823632c0660e5952acfb4fc9cf2e49..b3a0a8f7bcf6b65b2917430e77c7e75716179f5f 100644 GIT binary patch delta 590 zcmV-U0y{D6rF&BRT2XskIMF-*t0~8M#O+ibg0005yNklf1r&&Kn-GVWjYIOB$aJi{{e+y8U-T={-{I{J&}ND zV&dhv^V!UU$N6%(yB89a8yIG1cW3weW@g{cvduJ5z;*}rU~hlOymkf8@v(7J-E`@$2Z=*5%K(hz;e#gO_HK)zZPw$o9511i+v(*fKo{}I czd(Ngt!fggOqyaCi~s-t07*qoM6N<$f)$7XNdN!< literal 2637 zcmd^BiB}Tn76(x%1JS8WFqdi^oy;Z4+;XYRFdLl8nQ-AnX&92qDDE4MGgeb7xnxda zX{2eWqi8CoVwg*67Gt7V?!Djn&i&nc&b{?L z<}^$bsR;l8V4i6A^D5o_MYe%eXC=0(NTt*xPk8!lQ?0~pK{r)+I~na82>|Ta`9*+$ z-wX9rcQc;u$9-Z8i+z0oD+ei5CechoY+a{@puHZW^m)@A0pm6C@dCJ4c zioc53No^0O7~wJehzfRcLzu-@2<>an?0@fp5gr?a?7)cC@%)eni8$p3b6gI_05Kp8 zE~!O-SZgn+2w|Wpdobq{1v=|x)5ltA5L-5Cm3;ML5V5+VCoF$A|NG9n7=>J z|8AvBLj);=6{1(0WlO6uar+FwF0jO=sMERAXT>4BE;YgWXx>VxO?+9l%Yg_;LDuAyCqF znGhbpI?5JKl%~X7>m;*EH_m4D+V&zi2M}E6?d#f&jaLl$AR|Cgh2BNy9tUo%+6z!D zY8EX1f#ZeVqvvhX^1xE?pbs%s*=*|BAPy*SSf?}t?c*@L3`7K|Wbz|8NIsTgK3m!M zQ>maDZFiyq2s&?SXPg5W^{0jhW!jA;f5#?aBMEMQO@XJn2V?!I)xnugV_`sy5xL+< z8n8S^pSFn6O|*zymqeS-uJ6-Dvk)}LCZI#7($u>3ymdy;(vA14BrPbKo=v?DE1m!K zXUW;;f>dW`s~m#0anu>L+R!v&QYqdqM~)FQ4fBb(-e|29Ud^4$nQGIl<5&Kjo?bi%$84rxAD4LZ{yQd$(m8eq&9#e{FaVv^96J zGk}ZxiiVpSD^75jef3+b5F|U0X9+DiYMf&$G(ES;NzI6EIn#;#;&TE&=- zca^*m^A_i|$zkyMfuZ=cKQ>aD)7hX2OSXuKoorz!vsG4a@gj~85MeNd%|~PmGm#1* zZ#W~Q5q{T26Xwg^K#941)#*;lj@>%S4ZX&!g?AA#6R}x<8L;AM0r45t-B#$Je;ET? zrdTY;|0#mn3i)>pDb`s?g)@hX;2JQtLOAp2EXYBNS4p|MvIPl>fuAB`NHamvuw^2a zYfxXMiny?;@S9}zZ60%cyFLzHcVfYF_T8ZP6$KBb|%Vt)6vMwWt48(ar?NWJ;xE%Glp|b1R3hh7xq!-8R zphvbm!@o8d&tpDlyFgaPL6?`z3Pg+5ErLpbkyYW4KibHIQgqZ>n!& z9Ob9_mB$v`aV6>MrA}V*yLlz`=K^W0itg4lN=1v+I8)Yz9ZfalzheEX-lv4GlTWoT zd?UayP~Rst{!t4LB$`9X&TpuLh!0D%y*o-BMik(G2)Q=${kZSKTZ&_dEth<3jib^J zTClzJQK}|LGMfGEbzGIh4GJ79g|*hvb6Mn*LPhKK7-?A0O-N;b zgw8DP^-SilH@kK{|MhB>Llz~PWF?X8FU!F=G67rr#!}bJh`3`O?BSIo65R)Rx2nxi zt*(x9tZVkvON6Buh_kETkTaTo>gj|C{(?REq6@FT+AAZ9?qYM13J`~sa6iS%|5_s* zNprmI!`;xHWgD&?4hIJTKY|%c!-39g=$OHK>5Ta_EhNf#2LT=@mG=NYC5ir`c%A&) zx;!Xe2SL+dJ+&1=J|`cp(E!!rb(_-H><)?ADy&uh??{bS291}JzQmS?G@gFMmk<8F zFcF%LRLrM7+;fMOt74Ruo{$_rhS@{pH+3sQ$5Jzwo-5qU!9gxzU%*jrlHU)|?!bT*_Qt1NY(LE(PdV$15oiP=Lx+6|Eh zZUoop4r-Qv2uqP>`Z$iVK^A(n)lqLH^|MEBn?5?S&{Vm>&K?UAb`#%K&{}{I&li!NBkz*I7 z3sT?l0p;ohez70gdr&}E=>0{)r&e) zr$CzrLc0mH3#>7dT$ADf`tm3rV=Mg6pq2eK5w&K7D1X)kJgDxgng;=%r!ekSC-6W2 E6BjG#1^@s6 diff --git a/public/img/more_icon.png b/public/img/more_icon.png index 191cf73dd8c21696bcfc427e16044f9f6d0a0767..a9e6975e726b50e03cfc221521a06a9aa1a81a8c 100644 GIT binary patch literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^3Lwk@BpAX3RW*PVQEu7>8{`cPdG;`g@l2Et zuXmLAf3sIoLPFwV?Z@?5ddgE&tLHT~Ha@hM%5YdQ_4oh(|LaS%)xLQd8yFbu`Nb>j zu>BF6^x^&bKH1jwMg|5aY+Woq6@K=1Te1G%r!)4qz6&pW3e*GCwat;^gZ#bo2M!!q z)c>f>{k+7QrT+|ph8P$e-_Mtpkg#rBsiWD$Sw@HbW|)V402<~0dp~0qla0!MhnHtB z&Xdqx!+f>mP6OCsOh1D(38ZpAN{iuX3UU?2{hfb z_}J0)C4M25KixMpw7Pv?o;*KL$1LF3R5`ZPV+&Sa}}Z~tnu@EtHb89ZJ6T-G@y GGywnrucrk7 literal 6793 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 diff --git a/templates/includes/images_list.html.ep b/templates/includes/images_list.html.ep index 6efb6c9..cece072 100644 --- a/templates/includes/images_list.html.ep +++ b/templates/includes/images_list.html.ep @@ -17,7 +17,7 @@ sequentialUploads: true, progressall: function (e, data) { var progress = parseInt(data.loaded / data.total * 100, 10); - $('#progress .bar').css( + $('#progress .progress-bar').css( 'width', progress + '%' ); @@ -26,10 +26,10 @@ }); -
    +
    -
    +