From c56568b6f5d94a35221643296625c2dd48a41be1 Mon Sep 17 00:00:00 2001 From: nelsonlaquet Date: Fri, 26 Jul 2013 19:15:07 -0500 Subject: [PATCH] Further things --- app/config/app.php | 2 +- app/controllers/Api/Web/TracksController.php | 10 +- ...ontroller.php => FavouritesController.php} | 2 +- app/controllers/ImagesController.php | 20 + app/controllers/TracksController.php | 3 + app/controllers/UsersController.php | 19 + .../2013_06_27_015259_create_tracks_table.php | 1 - .../2013_07_26_230827_create_images_table.php | 44 + app/library/Assets.php | 2 + app/library/External.php | 14 + app/library/File.php | 32 + app/library/Gravatar.php | 20 + app/models/Commands/EditTrackCommand.php | 10 + app/models/Commands/UploadTrackCommand.php | 10 +- app/models/Entities/Image.php | 95 ++ app/models/Entities/Track.php | 41 +- app/models/Entities/User.php | 29 + app/routes.php | 10 +- app/views/shared/_app_layout.blade.php | 2 +- public/scripts/app/app.coffee | 18 +- .../controllers/account-content-tracks.coffee | 20 +- public/scripts/app/services/lightbox.coffee | 8 + public/scripts/base/jquery.colorbox.js | 1063 +++++++++++++++++ public/scripts/shared/layout.coffee | 5 + public/styles/account-tracks.less | 33 +- public/styles/base/colorbox.css | 44 + public/styles/base/images/border.png | Bin 0 -> 112 bytes public/styles/base/images/controls.png | Bin 0 -> 1633 bytes public/styles/base/images/loading.gif | Bin 0 -> 9427 bytes .../styles/base/images/loading_background.png | Bin 0 -> 157 bytes public/styles/base/images/overlay.png | Bin 0 -> 182 bytes public/styles/base/jquery-ui.css | 34 +- public/styles/layout.less | 3 - public/templates/account/content/tracks.html | 9 +- .../templates/account/favorites/_layout.html | 14 - .../templates/account/favorites/albums.html | 1 - .../account/favorites/playlists.html | 1 - .../templates/account/favorites/tracks.html | 1 - .../templates/account/favourites/_layout.html | 14 + .../templates/account/favourites/albums.html | 1 + .../account/favourites/playlists.html | 1 + .../templates/account/favourites/tracks.html | 1 + vendor/autoload.php | 2 +- vendor/composer/autoload_classmap.php | 13 +- vendor/composer/autoload_real.php | 6 +- 45 files changed, 1572 insertions(+), 86 deletions(-) rename app/controllers/{FavoritesController.php => FavouritesController.php} (81%) create mode 100644 app/controllers/ImagesController.php create mode 100644 app/controllers/UsersController.php create mode 100644 app/database/migrations/2013_07_26_230827_create_images_table.php create mode 100644 app/library/External.php create mode 100644 app/library/File.php create mode 100644 app/library/Gravatar.php create mode 100644 app/models/Entities/Image.php create mode 100644 public/scripts/app/services/lightbox.coffee create mode 100644 public/scripts/base/jquery.colorbox.js create mode 100644 public/styles/base/colorbox.css create mode 100644 public/styles/base/images/border.png create mode 100644 public/styles/base/images/controls.png create mode 100644 public/styles/base/images/loading.gif create mode 100644 public/styles/base/images/loading_background.png create mode 100644 public/styles/base/images/overlay.png delete mode 100644 public/templates/account/favorites/_layout.html delete mode 100644 public/templates/account/favorites/albums.html delete mode 100644 public/templates/account/favorites/playlists.html delete mode 100644 public/templates/account/favorites/tracks.html create mode 100644 public/templates/account/favourites/_layout.html create mode 100644 public/templates/account/favourites/albums.html create mode 100644 public/templates/account/favourites/playlists.html create mode 100644 public/templates/account/favourites/tracks.html diff --git a/app/config/app.php b/app/config/app.php index 20498393..ecfefce3 100644 --- a/app/config/app.php +++ b/app/config/app.php @@ -155,7 +155,7 @@ return array( 'DB' => 'Illuminate\Support\Facades\DB', 'Eloquent' => 'Illuminate\Database\Eloquent\Model', 'Event' => 'Illuminate\Support\Facades\Event', - 'File' => 'Illuminate\Support\Facades\File', +// 'File' => 'Illuminate\Support\Facades\File', 'Form' => 'Illuminate\Support\Facades\Form', 'Hash' => 'Illuminate\Support\Facades\Hash', 'HTML' => 'Illuminate\Support\Facades\HTML', diff --git a/app/controllers/Api/Web/TracksController.php b/app/controllers/Api/Web/TracksController.php index ae02da22..3f81b320 100644 --- a/app/controllers/Api/Web/TracksController.php +++ b/app/controllers/Api/Web/TracksController.php @@ -5,6 +5,8 @@ use Commands\DeleteTrackCommand; use Commands\EditTrackCommand; use Commands\UploadTrackCommand; + use Cover; + use Entities\Image; use Entities\Track; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Input; @@ -51,12 +53,13 @@ 'is_vocal' => $track->is_vocal, 'is_explicit' => $track->is_explicit, 'is_downloadable' => $track->is_downloadable, - 'is_published' => $track->published_at != null, + 'is_published' => $track->isPublished(), 'created_at' => $track->created_at, 'published_at' => $track->published_at, 'duration' => $track->duration, 'genre_id' => $track->genre_id, 'track_type_id' => $track->track_type_id, + 'cover_url' => $track->getCoverUrl(Image::SMALL) ]; } @@ -78,7 +81,7 @@ 'slug' => $track->slug, 'is_vocal' => (bool)$track->is_vocal, 'is_explicit' => (bool)$track->is_explicit, - 'is_downloadable' => $track->published_at == null ? true : (bool)$track->is_downloadable, + 'is_downloadable' => !$track->isPublished() ? true : (bool)$track->is_downloadable, 'is_published' => $track->published_at != null, 'created_at' => $track->created_at, 'published_at' => $track->published_at, @@ -88,7 +91,8 @@ 'license_id' => $track->license_id != null ? $track->license_id : 3, 'description' => $track->description, 'lyrics' => $track->lyrics, - 'released_at' => $track->released_at + 'released_at' => $track->released_at, + 'cover_url' => $track->hasCover() ? $track->getCoverUrl(Image::NORMAL) : null ], 200); } diff --git a/app/controllers/FavoritesController.php b/app/controllers/FavouritesController.php similarity index 81% rename from app/controllers/FavoritesController.php rename to app/controllers/FavouritesController.php index c4f89cee..9439c052 100644 --- a/app/controllers/FavoritesController.php +++ b/app/controllers/FavouritesController.php @@ -1,6 +1,6 @@ getFile($coverType['id']), $image->mime, $image->filename); + } + } \ No newline at end of file diff --git a/app/controllers/TracksController.php b/app/controllers/TracksController.php index f2a2cf6d..8f14352b 100644 --- a/app/controllers/TracksController.php +++ b/app/controllers/TracksController.php @@ -1,5 +1,8 @@ getAvatarFile($coverType['id']), 'image/png', 'cover.png'); + } + } \ No newline at end of file diff --git a/app/database/migrations/2013_06_27_015259_create_tracks_table.php b/app/database/migrations/2013_06_27_015259_create_tracks_table.php index 7952a93d..32e4d4a9 100644 --- a/app/database/migrations/2013_06_27_015259_create_tracks_table.php +++ b/app/database/migrations/2013_06_27_015259_create_tracks_table.php @@ -39,7 +39,6 @@ class CreateTracksTable extends Migration { $table->text('lyrics')->nullable(); $table->boolean('is_vocal'); $table->boolean('is_explicit'); - $table->integer('cover_id')->nullable()->unsigned()->default(NULL); $table->boolean('is_downloadable'); $table->float('duration')->unsigned(); diff --git a/app/database/migrations/2013_07_26_230827_create_images_table.php b/app/database/migrations/2013_07_26_230827_create_images_table.php new file mode 100644 index 00000000..bf1f9e0b --- /dev/null +++ b/app/database/migrations/2013_07_26_230827_create_images_table.php @@ -0,0 +1,44 @@ +increments('id'); + $table->string('filename', 256); + $table->string('mime', 100); + $table->string('extension', 32); + $table->integer('size'); + $table->string('hash', 32); + $table->integer('uploaded_by')->unsigned(); + $table->timestamps(); + + $table->foreign('uploaded_by')->references('id')->on('users'); + }); + + Schema::table('users', function($table) { + $table->integer('avatar_id')->unsigned()->nullable(); + $table->foreign('avatar_id')->references('id')->on('images'); + }); + + DB::table('tracks')->update(['cover_id' => null]); + + Schema::table('tracks', function($table) { + $table->foreign('cover_id')->references('id')->on('images'); + }); + } + + public function down() { + Schema::table('tracks', function($table) { + $table->dropForeign('tracks_cover_id_foreign'); + }); + + Schema::table('users', function($table) { + $table->dropForeign('users_avatar_id_foreign'); + $table->dropColumn('avatar_id'); + }); + + Schema::drop('images'); + } +} \ No newline at end of file diff --git a/app/library/Assets.php b/app/library/Assets.php index 9b1a7b55..f6c12376 100644 --- a/app/library/Assets.php +++ b/app/library/Assets.php @@ -45,6 +45,7 @@ return new AssetCollection([ new FileAsset('scripts/base/jquery-2.0.2.js'), new FileAsset('scripts/base/jquery-ui.js'), + new FileAsset('scripts/base/jquery.colorbox.js'), new FileAsset('scripts/base/underscore.js'), new FileAsset('scripts/base/angular.js'), new FileAsset('scripts/base/ui-bootstrap-tpls-0.4.0.js'), @@ -72,6 +73,7 @@ $css = new AssetCollection([ new FileAsset('styles/base/jquery-ui.css'), + new FileAsset('styles/base/colorbox.css'), new FileAsset('styles/app.less'), new CacheBusterAsset($lastModifiedCollection->getLastModified()) ], [new \Assetic\Filter\LessFilter('node')]); diff --git a/app/library/External.php b/app/library/External.php new file mode 100644 index 00000000..e2a7dbe8 --- /dev/null +++ b/app/library/External.php @@ -0,0 +1,14 @@ + + */ + class File extends \Illuminate\Support\Facades\File + { + + public static function inline($path, $mime, $name = null) + { + if (is_null($name)) + { + $name = basename($path); + } + + $response = Response::make(static::get($path)); + + $response->header('Content-Type', $mime); + $response->header('Content-Disposition', 'inline; filename="'.$name.'"'); + $response->header('Content-Transfer-Encoding', 'binary'); + $response->header('Expires', 0); + $response->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0'); + $response->header('Pragma', 'public'); + $response->header('Content-Length', filesize($path)); + + return $response; + } + + } diff --git a/app/library/Gravatar.php b/app/library/Gravatar.php new file mode 100644 index 00000000..93794ce6 --- /dev/null +++ b/app/library/Gravatar.php @@ -0,0 +1,20 @@ +published_at = new \DateTime(); } + if (isset($this->_input['cover'])) { + $cover = $this->_input['cover']; + $track->cover_id = Image::Upload($cover, Auth::user())->id; + } else + $track->cover_id = null; + $track->save(); return CommandResponse::succeed(); diff --git a/app/models/Commands/UploadTrackCommand.php b/app/models/Commands/UploadTrackCommand.php index f69c1767..935dbf36 100644 --- a/app/models/Commands/UploadTrackCommand.php +++ b/app/models/Commands/UploadTrackCommand.php @@ -44,14 +44,12 @@ $track->save(); $destination = $track->getDirectory(); - - if (!is_dir($destination)) - mkdir($destination, 755); + $track->ensureDirectoryExists(); $source = $trackFile->getPathname(); $index = 0; - $procs = []; + $processes = []; foreach (Track::$Formats as $name => $format) { $target = $destination . '/' . $track->getFilenameFor($name); @@ -65,10 +63,10 @@ $pipes = []; $proc = proc_open($command, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'a']], $pipes); - $procs[] = $proc; + $processes[] = $proc; } - foreach ($procs as $proc) + foreach ($processes as $proc) proc_close($proc); } catch (\Exception $e) { diff --git a/app/models/Entities/Image.php b/app/models/Entities/Image.php new file mode 100644 index 00000000..a6693e8e --- /dev/null +++ b/app/models/Entities/Image.php @@ -0,0 +1,95 @@ + ['id' => self::NORMAL, 'name' => 'normal', 'width' => 350, 'height' => 350], + self::ORIGINAL => ['id' => self::ORIGINAL, 'name' => 'original', 'width' => null, 'height' => null], + self::SMALL => ['id' => self::SMALL, 'name' => 'small', 'width' => 100, 'height' => 100], + self::THUMBNAIL => ['id' => self::THUMBNAIL, 'name' => 'thumbnail', 'width' => 50, 'height' => 50] + ]; + + public static function GetImageTypeFromName($name) { + foreach (self::$ImageTypes as $cover) { + if ($cover['name'] != $name) + continue; + + return $cover; + } + + return null; + } + + public static function Upload($file, $user) { + $hash = md5_file($file->getPathname()); + $image = Image::whereHash($hash)->whereUploadedBy($user->id)->first(); + + if ($image) + return $image; + + $image = new Image(); + try { + $image->uploaded_by = $user->id; + $image->size = $file->getSize(); + $image->filename = $file->getClientOriginalName(); + $image->extension = $file->getClientOriginalExtension(); + $image->mime = $file->getMimeType(); + $image->hash = $hash; + $image->save(); + + $image->ensureDirectoryExists(); + foreach (self::$ImageTypes as $coverType) { + $command = 'convert 2>&1 "' . $file->getPathname() . '" -background transparent -flatten +matte -strip -quality 95 -format png '; + if (isset($coverType['width']) && isset($coverType['height'])) + $command .= '-thumbnail ' . $coverType['width'] . 'x' . $coverType['height'] . '^ -gravity center -extent ' . $coverType['width'] . 'x' . $coverType['height'] . ' '; + + $command .= '"' . $image->getFile($coverType['id']) . '"'; + External::execute($command); + } + + return $image; + } + catch (\Exception $e) { + $image->delete(); + throw $e; + } + } + + protected $table = 'images'; + + public function getUrl($type = Cover::NORMAL) { + $type = self::$ImageTypes[$type]; + return URL::to('i' . $this->id . '/' . $type['name'] . '.png'); + } + + public function getFile($type = Cover::NORMAL) { + return $this->getDirectory() . '/' . $this->getFilename($type); + } + + public function getFilename($type = Cover::NORMAL) { + $typeInfo = self::$ImageTypes[$type]; + return $this->id . '_' . $typeInfo['name'] . '.png'; + } + + public function getDirectory() { + $dir = (string) ( floor( $this->id / 100 ) * 100 ); + return Config::get('app.files_directory') . '/images/' . $dir; + } + + public function ensureDirectoryExists() { + $destination = $this->getDirectory(); + + if (!is_dir($destination)) + mkdir($destination, 755, true); + } + } \ No newline at end of file diff --git a/app/models/Entities/Track.php b/app/models/Entities/Track.php index 0f9cdae8..e0afafc0 100644 --- a/app/models/Entities/Track.php +++ b/app/models/Entities/Track.php @@ -2,6 +2,7 @@ namespace Entities; + use Cover; use Whoops\Example\Exception; class Track extends \Eloquent { @@ -16,22 +17,48 @@ ]; public static function summary() { - return self::select('id', 'title', 'user_id', 'slug', 'is_vocal', 'is_explicit', 'created_at', 'published_at', 'duration', 'is_downloadable', 'genre_id', 'track_type_id'); + return self::select('id', 'title', 'user_id', 'slug', 'is_vocal', 'is_explicit', 'created_at', 'published_at', 'duration', 'is_downloadable', 'genre_id', 'track_type_id', 'cover_id'); } protected $table = 'tracks'; + public function ensureDirectoryExists() { + $destination = $this->getDirectory(); + + if (!is_dir($destination)) + mkdir($destination, 755); + } + + public function hasCover() { + return $this->cover_id != null; + } + + public function cover() { + return $this->belongsTo('Entities\Image'); + } + + public function isPublished() { + return $this->published_at != null && $this->deleted_at == null; + } + + public function getCoverUrl($type = Cover::NORMAL) { + if (!$this->hasCover()) + return $this->user->getAvatarUrl($type); + + return $this->cover->getUrl($type); + } + + public function getDirectory() { + $dir = (string) ( floor( $this->id / 100 ) * 100 ); + return \Config::get('app.files_directory') . '/tracks/' . $dir; + } + public function getDates() { return ['created_at', 'deleted_at', 'published_at', 'released_at']; } public function user() { - return $this->belongsTo('User'); - } - - public function getDirectory() { - $dir = (string) ( floor( $this->id / 100 ) * 100 ); - return \Config::get('app.files_directory') . '/tracks/' . $dir; + return $this->belongsTo('Entities\User'); } public function getFilenameFor($format) { diff --git a/app/models/Entities/User.php b/app/models/Entities/User.php index 9f5fb561..462d705c 100644 --- a/app/models/Entities/User.php +++ b/app/models/Entities/User.php @@ -2,6 +2,8 @@ namespace Entities; + use Cover; + use Gravatar; use Illuminate\Auth\UserInterface; use Illuminate\Auth\Reminders\RemindableInterface; @@ -9,6 +11,14 @@ protected $table = 'users'; protected $hidden = ['password_hash', 'password_salt', 'bio']; + public function avatar() { + return $this->hasOne('Entities\Image'); + } + + public function cover() { + return $this->belongsTo('Entities\Image'); + } + public function getAuthIdentifier() { return $this->getKey(); } @@ -20,4 +30,23 @@ public function getReminderEmail() { return $this->email; } + + public function getAvatarUrl($type = Cover::NORMAL) { + if (!$this->uses_gravatar) + return $this->cover->getUrl(); + + $email = $this->gravatar; + if (!strlen($email)) + $email = $this->email; + + return Gravatar::getUrl($email, Image::$ImageTypes[$type]['width']); + } + + public function getAvatarFile($type = Cover::NORMAL) { + if ($this->uses_gravatar) + return $this->user->getAvatar($type); + + $cover = Cover::$Covers[$type]; + return URL::to('t' . $this->id . '/cover_' . $cover['name'] . '.png?' . $this->cover_id); + } } \ No newline at end of file diff --git a/app/routes.php b/app/routes.php index 68da1bd7..7734eb1d 100644 --- a/app/routes.php +++ b/app/routes.php @@ -25,6 +25,10 @@ Route::get('/about', function() { return View::make('pages.about'); }); Route::get('/faq', function() { return View::make('pages.faq'); }); + Route::get('i{id}/{type}.png', 'ImagesController@getImage'); + + Route::get('u{id}/avatar_{type}.png', 'UsersController@getAvatar'); + Route::group(['prefix' => 'api/web'], function() { Route::get('/taxonomies/all', 'Api\Web\TaxonomiesController@getAll'); @@ -47,9 +51,9 @@ Route::group(['prefix' => 'account'], function() { Route::group(['before' => 'auth'], function(){ - Route::get('/favorites', 'FavoritesController@getTracks'); - Route::get('/favorites/albums', 'FavoritesController@getAlbums'); - Route::get('/favorites/playlists', 'FavoritesController@getPlaylists'); + Route::get('/favourites', 'FavouritesController@getTracks'); + Route::get('/favourites/albums', 'FavouritesController@getAlbums'); + Route::get('/favourites/playlists', 'FavouritesController@getPlaylists'); Route::get('/content/tracks', 'ContentController@getTracks'); Route::get('/content/tracks/{id}', 'ContentController@getTracks'); diff --git a/app/views/shared/_app_layout.blade.php b/app/views/shared/_app_layout.blade.php index 92c74b89..93cb8ed7 100644 --- a/app/views/shared/_app_layout.blade.php +++ b/app/views/shared/_app_layout.blade.php @@ -42,7 +42,7 @@
  • Account

  • -
  • Favorites
  • +
  • Favourites
  • Your Content
  • Settings
  • @endif diff --git a/public/scripts/app/app.coffee b/public/scripts/app/app.coffee index ee202c43..dd273d13 100644 --- a/public/scripts/app/app.coffee +++ b/public/scripts/app/app.coffee @@ -30,22 +30,22 @@ angular.module 'ponyfm', ['ui.bootstrap', 'ui.state', 'ui.date'], [ url: '/playlists' templateUrl: '/templates/account/content/playlists.html' - state.state 'account-favorites', - url: '/account/favorites' + state.state 'account-favourites', + url: '/account/favourites' abstract: true - templateUrl: '/templates/account/favorites/_layout.html' + templateUrl: '/templates/account/favourites/_layout.html' - state.state 'account-favorites.tracks', + state.state 'account-favourites.tracks', url: '' - templateUrl: '/templates/account/favorites/tracks.html' + templateUrl: '/templates/account/favourites/tracks.html' - state.state 'account-favorites.playlists', + state.state 'account-favourites.playlists', url: '/playlists' - templateUrl: '/templates/account/favorites/playlists.html' + templateUrl: '/templates/account/favourites/playlists.html' - state.state 'account-favorites.albums', + state.state 'account-favourites.albums', url: '/albums' - templateUrl: '/templates/account/favorites/albums.html' + templateUrl: '/templates/account/favourites/albums.html' # Tracks diff --git a/public/scripts/app/controllers/account-content-tracks.coffee b/public/scripts/app/controllers/account-content-tracks.coffee index 828ab080..3b489188 100644 --- a/public/scripts/app/controllers/account-content-tracks.coffee +++ b/public/scripts/app/controllers/account-content-tracks.coffee @@ -1,6 +1,11 @@ angular.module('ponyfm').controller "account-content-tracks", [ - '$scope', '$state', 'taxonomies', '$dialog' - ($scope, $state, taxonomies, $dialog) -> + '$scope', '$state', 'taxonomies', '$dialog', 'lightbox' + ($scope, $state, taxonomies, $dialog, lightbox) -> + $('#coverPreview').load () -> + $scope.$apply -> $scope.isCoverLoaded = true + window.alignVertically(this) + + $scope.isCoverLoaded = false $scope.selectedTrack = null $scope.isDirty = false $scope.taxonomies = @@ -11,6 +16,10 @@ angular.module('ponyfm').controller "account-content-tracks", [ $scope.updateIsVocal = () -> delete $scope.errors.lyrics if !$scope.edit.is_vocal + $scope.previewCover = () -> + return if !$scope.edit.cover + lightbox.openDataUrl $('#coverPreview').attr 'src' + $scope.updateTrack = (track) -> xhr = new XMLHttpRequest() xhr.onload = -> $scope.$apply -> @@ -49,6 +58,8 @@ angular.module('ponyfm').controller "account-content-tracks", [ if file.type != 'image/png' $scope.errors.cover = 'Cover image must be a png!' + $scope.isCoverLoaded = false + $scope.edit.cover = null return delete $scope.errors.cover @@ -58,6 +69,10 @@ angular.module('ponyfm').controller "account-content-tracks", [ reader.readAsDataURL file $scope.edit.cover = file + $scope.clearTrackCover = () -> + $scope.isCoverLoaded = false + delete $scope.edit.cover + $scope.filters = published: [ {title: 'Either', query: ''}, @@ -129,6 +144,7 @@ angular.module('ponyfm').controller "account-content-tracks", [ selectTrack = (t) -> $scope.selectedTrack = t + $scope.isCoverLoaded = false return if !t $.getJSON('/api/web/tracks/edit/' + t.id) .done (track) -> $scope.$apply -> diff --git a/public/scripts/app/services/lightbox.coffee b/public/scripts/app/services/lightbox.coffee new file mode 100644 index 00000000..94e8293d --- /dev/null +++ b/public/scripts/app/services/lightbox.coffee @@ -0,0 +1,8 @@ +angular.module('ponyfm').factory('lightbox', [ + () -> + openDataUrl: (src) -> + $.colorbox + html: '' + transition: 'none' +]) + diff --git a/public/scripts/base/jquery.colorbox.js b/public/scripts/base/jquery.colorbox.js new file mode 100644 index 00000000..db00dacf --- /dev/null +++ b/public/scripts/base/jquery.colorbox.js @@ -0,0 +1,1063 @@ +/*! + Colorbox v1.4.27 - 2013-07-16 + jQuery lightbox and modal window plugin + (c) 2013 Jack Moore - http://www.jacklmoore.com/colorbox + license: http://www.opensource.org/licenses/mit-license.php +*/ +(function ($, document, window) { + var + // Default settings object. + // See http://jacklmoore.com/colorbox for details. + defaults = { + transition: "elastic", + speed: 300, + fadeOut: 300, + width: false, + initialWidth: "600", + innerWidth: false, + maxWidth: false, + height: false, + initialHeight: "450", + innerHeight: false, + maxHeight: false, + scalePhotos: true, + scrolling: true, + inline: false, + html: false, + iframe: false, + fastIframe: true, + photo: false, + href: false, + title: false, + rel: false, + opacity: 0.9, + preloading: true, + className: false, + + // alternate image paths for high-res displays + retinaImage: false, + retinaUrl: false, + retinaSuffix: '@2x.$1', + + // internationalization + current: "image {current} of {total}", + previous: "previous", + next: "next", + close: "close", + xhrError: "This content failed to load.", + imgError: "This image failed to load.", + + open: false, + returnFocus: true, + trapFocus: true, + reposition: true, + loop: true, + slideshow: false, + slideshowAuto: true, + slideshowSpeed: 2500, + slideshowStart: "start slideshow", + slideshowStop: "stop slideshow", + photoRegex: /\.(gif|png|jp(e|g|eg)|bmp|ico|webp)((#|\?).*)?$/i, + + onOpen: false, + onLoad: false, + onComplete: false, + onCleanup: false, + onClosed: false, + + overlayClose: true, + escKey: true, + arrowKey: true, + top: false, + bottom: false, + left: false, + right: false, + fixed: false, + data: undefined, + closeButton: true + }, + + // Abstracting the HTML and event identifiers for easy rebranding + colorbox = 'colorbox', + prefix = 'cbox', + boxElement = prefix + 'Element', + + // Events + event_open = prefix + '_open', + event_load = prefix + '_load', + event_complete = prefix + '_complete', + event_cleanup = prefix + '_cleanup', + event_closed = prefix + '_closed', + event_purge = prefix + '_purge', + + // Cached jQuery Object Variables + $overlay, + $box, + $wrap, + $content, + $topBorder, + $leftBorder, + $rightBorder, + $bottomBorder, + $related, + $window, + $loaded, + $loadingBay, + $loadingOverlay, + $title, + $current, + $slideshow, + $next, + $prev, + $close, + $groupControls, + $events = $(''), + + // Variables for cached values or use across multiple functions + settings, + interfaceHeight, + interfaceWidth, + loadedHeight, + loadedWidth, + element, + index, + photo, + open, + active, + closing, + loadingTimer, + publicMethod, + div = "div", + className, + requests = 0, + previousCSS = {}, + init; + + // **************** + // HELPER FUNCTIONS + // **************** + + // Convenience function for creating new jQuery objects + function $tag(tag, id, css) { + var element = document.createElement(tag); + + if (id) { + element.id = prefix + id; + } + + if (css) { + element.style.cssText = css; + } + + return $(element); + } + + // Get the window height using innerHeight when available to avoid an issue with iOS + // http://bugs.jquery.com/ticket/6724 + function winheight() { + return window.innerHeight ? window.innerHeight : $(window).height(); + } + + // Determine the next and previous members in a group. + function getIndex(increment) { + var + max = $related.length, + newIndex = (index + increment) % max; + + return (newIndex < 0) ? max + newIndex : newIndex; + } + + // Convert '%' and 'px' values to integers + function setSize(size, dimension) { + return Math.round((/%/.test(size) ? ((dimension === 'x' ? $window.width() : winheight()) / 100) : 1) * parseInt(size, 10)); + } + + // Checks an href to see if it is a photo. + // There is a force photo option (photo: true) for hrefs that cannot be matched by the regex. + function isImage(settings, url) { + return settings.photo || settings.photoRegex.test(url); + } + + function retinaUrl(settings, url) { + return settings.retinaUrl && window.devicePixelRatio > 1 ? url.replace(settings.photoRegex, settings.retinaSuffix) : url; + } + + function trapFocus(e) { + if ('contains' in $box[0] && !$box[0].contains(e.target)) { + e.stopPropagation(); + $box.focus(); + } + } + + // Assigns function results to their respective properties + function makeSettings() { + var i, + data = $.data(element, colorbox); + + if (data == null) { + settings = $.extend({}, defaults); + if (console && console.log) { + console.log('Error: cboxElement missing settings object'); + } + } else { + settings = $.extend({}, data); + } + + for (i in settings) { + if ($.isFunction(settings[i]) && i.slice(0, 2) !== 'on') { // checks to make sure the function isn't one of the callbacks, they will be handled at the appropriate time. + settings[i] = settings[i].call(element); + } + } + + settings.rel = settings.rel || element.rel || $(element).data('rel') || 'nofollow'; + settings.href = settings.href || $(element).attr('href'); + settings.title = settings.title || element.title; + + if (typeof settings.href === "string") { + settings.href = $.trim(settings.href); + } + } + + function trigger(event, callback) { + // for external use + $(document).trigger(event); + + // for internal use + $events.trigger(event); + + if ($.isFunction(callback)) { + callback.call(element); + } + } + + // Slideshow functionality + function slideshow() { + var + timeOut, + className = prefix + "Slideshow_", + click = "click." + prefix, + clear, + set, + start, + stop; + + if (settings.slideshow && $related[1]) { + clear = function () { + clearTimeout(timeOut); + }; + + set = function () { + if (settings.loop || $related[index + 1]) { + timeOut = setTimeout(publicMethod.next, settings.slideshowSpeed); + } + }; + + start = function () { + $slideshow + .html(settings.slideshowStop) + .unbind(click) + .one(click, stop); + + $events + .bind(event_complete, set) + .bind(event_load, clear) + .bind(event_cleanup, stop); + + $box.removeClass(className + "off").addClass(className + "on"); + }; + + stop = function () { + clear(); + + $events + .unbind(event_complete, set) + .unbind(event_load, clear) + .unbind(event_cleanup, stop); + + $slideshow + .html(settings.slideshowStart) + .unbind(click) + .one(click, function () { + publicMethod.next(); + start(); + }); + + $box.removeClass(className + "on").addClass(className + "off"); + }; + + if (settings.slideshowAuto) { + start(); + } else { + stop(); + } + } else { + $box.removeClass(className + "off " + className + "on"); + } + } + + function launch(target) { + if (!closing) { + + element = target; + + makeSettings(); + + $related = $(element); + + index = 0; + + if (settings.rel !== 'nofollow') { + $related = $('.' + boxElement).filter(function () { + var data = $.data(this, colorbox), + relRelated; + + if (data) { + relRelated = $(this).data('rel') || data.rel || this.rel; + } + + return (relRelated === settings.rel); + }); + index = $related.index(element); + + // Check direct calls to Colorbox. + if (index === -1) { + $related = $related.add(element); + index = $related.length - 1; + } + } + + $overlay.css({ + opacity: parseFloat(settings.opacity), + cursor: settings.overlayClose ? "pointer" : "auto", + visibility: 'visible' + }).show(); + + + if (className) { + $box.add($overlay).removeClass(className); + } + if (settings.className) { + $box.add($overlay).addClass(settings.className); + } + className = settings.className; + + if (settings.closeButton) { + $close.html(settings.close).appendTo($content); + } else { + $close.appendTo('
    '); + } + + if (!open) { + open = active = true; // Prevents the page-change action from queuing up if the visitor holds down the left or right keys. + + // Show colorbox so the sizes can be calculated in older versions of jQuery + $box.css({visibility:'hidden', display:'block'}); + + $loaded = $tag(div, 'LoadedContent', 'width:0; height:0; overflow:hidden'); + $content.css({width:'', height:''}).append($loaded); + + // Cache values needed for size calculations + interfaceHeight = $topBorder.height() + $bottomBorder.height() + $content.outerHeight(true) - $content.height(); + interfaceWidth = $leftBorder.width() + $rightBorder.width() + $content.outerWidth(true) - $content.width(); + loadedHeight = $loaded.outerHeight(true); + loadedWidth = $loaded.outerWidth(true); + + // Opens inital empty Colorbox prior to content being loaded. + settings.w = setSize(settings.initialWidth, 'x'); + settings.h = setSize(settings.initialHeight, 'y'); + publicMethod.position(); + + slideshow(); + + trigger(event_open, settings.onOpen); + + $groupControls.add($title).hide(); + + $box.focus(); + + + if (settings.trapFocus) { + // Confine focus to the modal + // Uses event capturing that is not supported in IE8- + if (document.addEventListener) { + + document.addEventListener('focus', trapFocus, true); + + $events.one(event_closed, function () { + document.removeEventListener('focus', trapFocus, true); + }); + } + } + + // Return focus on closing + if (settings.returnFocus) { + $events.one(event_closed, function () { + $(element).focus(); + }); + } + } + + load(); + } + } + + // Colorbox's markup needs to be added to the DOM prior to being called + // so that the browser will go ahead and load the CSS background images. + function appendHTML() { + if (!$box && document.body) { + init = false; + $window = $(window); + $box = $tag(div).attr({ + id: colorbox, + 'class': $.support.opacity === false ? prefix + 'IE' : '', // class for optional IE8 & lower targeted CSS. + role: 'dialog', + tabindex: '-1' + }).hide(); + $overlay = $tag(div, "Overlay").hide(); + $loadingOverlay = $([$tag(div, "LoadingOverlay")[0],$tag(div, "LoadingGraphic")[0]]); + $wrap = $tag(div, "Wrapper"); + $content = $tag(div, "Content").append( + $title = $tag(div, "Title"), + $current = $tag(div, "Current"), + $prev = $('