├── VERSION ├── install.rb ├── test ├── fixtures │ ├── text.txt │ ├── bad.png │ ├── 12k.png │ ├── 5k.png │ ├── 50x50.png │ ├── twopage.pdf │ └── s3.yml └── papermill_test.rb ├── rails └── init.rb ├── tasks └── papermill_tasks.rake ├── uninstall.rb ├── init.rb ├── public ├── .DS_Store ├── facebox │ ├── b.png │ ├── bl.png │ ├── br.png │ ├── tl.png │ ├── tr.png │ ├── loading.gif │ ├── closelabel.gif │ ├── README.txt │ ├── facebox.css │ └── facebox.js ├── Jcrop │ ├── images │ │ └── Jcrop.gif │ ├── jquery.Jcrop.css │ ├── jquery.Jcrop.min.js │ └── jquery.Jcrop.js ├── swfupload │ ├── swfupload.js │ └── swfupload.swf ├── papermill │ ├── images │ │ ├── delete.png │ │ ├── upload.png │ │ ├── background.png │ │ ├── mass-delete.png │ │ ├── mass-edit.png │ │ ├── upload-blank.png │ │ ├── container-background.jpg │ │ └── mass-thumbnail-reset.png │ ├── README │ ├── papermill.css │ └── papermill.js └── jgrowl │ ├── jquery.jgrowl.css │ ├── jquery.jgrowl_minimized.js │ └── jquery.jgrowl.js ├── .gitignore ├── generators ├── papermill_table │ ├── USAGE │ ├── papermill_table_generator.rb │ └── templates │ │ └── migrate │ │ └── papermill_migration.rb.erb ├── papermill_assets │ ├── USAGE │ └── papermill_assets_generator.rb └── papermill_initializer │ ├── USAGE │ ├── papermill_initializer_generator.rb │ └── papermill_initializer.rb ├── app ├── views │ └── papermill │ │ ├── _raw_asset.html.erb │ │ ├── _thumbnail_asset.html.erb │ │ ├── _asset.html.erb │ │ ├── _form.html.erb │ │ ├── browser.html.erb │ │ ├── edit.html.erb │ │ └── crop.html.erb └── controllers │ └── papermill_controller.rb ├── TODO.txt ├── lib ├── papermill │ ├── papermill_association.rb │ ├── flash_session_cookie_middleware.rb │ ├── extensions.rb │ ├── papermill_helper.rb │ ├── papermill_options.rb │ ├── papermill_paperclip_processor.rb │ ├── papermill.rb │ ├── papermill_asset.rb │ └── form_builder.rb └── papermill.rb ├── config ├── routes.rb └── locales │ └── papermill.yml ├── MIT-LICENSE ├── Rakefile ├── demo.txt └── README.rdoc /VERSION: -------------------------------------------------------------------------------- 1 | 2.1.1 -------------------------------------------------------------------------------- /install.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/text.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | require 'papermill' -------------------------------------------------------------------------------- /tasks/papermill_tasks.rake: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /uninstall.rb: -------------------------------------------------------------------------------- 1 | # Uninstall hook code here 2 | -------------------------------------------------------------------------------- /test/fixtures/bad.png: -------------------------------------------------------------------------------- 1 | This is not an image. 2 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/rails/init.rb" 2 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/.DS_Store -------------------------------------------------------------------------------- /public/facebox/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/facebox/b.png -------------------------------------------------------------------------------- /public/facebox/bl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/facebox/bl.png -------------------------------------------------------------------------------- /public/facebox/br.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/facebox/br.png -------------------------------------------------------------------------------- /public/facebox/tl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/facebox/tl.png -------------------------------------------------------------------------------- /public/facebox/tr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/facebox/tr.png -------------------------------------------------------------------------------- /test/fixtures/12k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/test/fixtures/12k.png -------------------------------------------------------------------------------- /test/fixtures/5k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/test/fixtures/5k.png -------------------------------------------------------------------------------- /test/fixtures/50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/test/fixtures/50x50.png -------------------------------------------------------------------------------- /public/facebox/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/facebox/loading.gif -------------------------------------------------------------------------------- /test/fixtures/twopage.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/test/fixtures/twopage.pdf -------------------------------------------------------------------------------- /public/Jcrop/images/Jcrop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/Jcrop/images/Jcrop.gif -------------------------------------------------------------------------------- /public/facebox/closelabel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/facebox/closelabel.gif -------------------------------------------------------------------------------- /public/swfupload/swfupload.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/swfupload/swfupload.js -------------------------------------------------------------------------------- /public/swfupload/swfupload.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/swfupload/swfupload.swf -------------------------------------------------------------------------------- /public/papermill/images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/papermill/images/delete.png -------------------------------------------------------------------------------- /public/papermill/images/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/papermill/images/upload.png -------------------------------------------------------------------------------- /public/papermill/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/papermill/images/background.png -------------------------------------------------------------------------------- /public/papermill/images/mass-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/papermill/images/mass-delete.png -------------------------------------------------------------------------------- /public/papermill/images/mass-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/papermill/images/mass-edit.png -------------------------------------------------------------------------------- /public/papermill/images/upload-blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/papermill/images/upload-blank.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test/*.log 3 | log/*.log 4 | tmp/**/* 5 | config/database.yml 6 | db/*.sqlite3 7 | test.sqlte3 8 | papermill.gemspec 9 | -------------------------------------------------------------------------------- /generators/papermill_table/USAGE: -------------------------------------------------------------------------------- 1 | script/generate papermill_table PapermillMigration 2 | 3 | This will create the Papermill table migration file! 4 | -------------------------------------------------------------------------------- /public/papermill/images/container-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/papermill/images/container-background.jpg -------------------------------------------------------------------------------- /public/papermill/images/mass-thumbnail-reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbenezech/papermill/HEAD/public/papermill/images/mass-thumbnail-reset.png -------------------------------------------------------------------------------- /generators/papermill_assets/USAGE: -------------------------------------------------------------------------------- 1 | script/generate papermill_assets 2 | 3 | This will copy Papermill needed assets to public/papermill/ 4 | The old public/papermill directory will be deleted if present! 5 | -------------------------------------------------------------------------------- /app/views/papermill/_raw_asset.html.erb: -------------------------------------------------------------------------------- 1 | <%= link_to(raw_asset.name, edit_papermill_url(raw_asset), :class => "name", :onClick => "popup(jQuery(this).attr('rel')); return false;", :rel => edit_papermill_url(raw_asset)) -%> -------------------------------------------------------------------------------- /generators/papermill_initializer/USAGE: -------------------------------------------------------------------------------- 1 | script/generate papermill_initializer 2 | 3 | This will copy Papermill option hash to your config/initializers directory 4 | The old file directory will be deleted if present! 5 | -------------------------------------------------------------------------------- /public/facebox/README.txt: -------------------------------------------------------------------------------- 1 | Please visit http://famspam.com/facebox/ or open index.html in your favorite browser. 2 | 3 | Need help? Join our Google Groups mailing list: 4 | http://groups.google.com/group/facebox/ 5 | -------------------------------------------------------------------------------- /test/fixtures/s3.yml: -------------------------------------------------------------------------------- 1 | development: 2 | key: 54321 3 | production: 4 | key: 12345 5 | test: 6 | bucket: <%= ENV['S3_BUCKET'] %> 7 | access_key_id: <%= ENV['S3_KEY'] %> 8 | secret_access_key: <%= ENV['S3_SECRET'] %> 9 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | # extend watermark possibilities 2 | # refresh images when cropped 3 | # authorize images before edit/crop 4 | # documentation for papermill_associations 5 | # file search helper for papermill_associations 6 | # tests, loads of them 7 | # rails3 branch when RC is out -------------------------------------------------------------------------------- /lib/papermill/papermill_association.rb: -------------------------------------------------------------------------------- 1 | class PapermillAssociation < ActiveRecord::Base 2 | belongs_to :papermill_asset 3 | belongs_to :assetable, :polymorphic => true 4 | 5 | def assetable_type=(sType) 6 | super(sType.to_s.classify.constantize.base_class.to_s) 7 | end 8 | end -------------------------------------------------------------------------------- /public/papermill/README: -------------------------------------------------------------------------------- 1 | DO NOT !! 2 | 3 | Modify files in this folder, they are generated by papermill 4 | 5 | 6 | DO !! 7 | 8 | Load them before your own and override them in your application. 9 | 10 | And each time you update Papermill's gem : 11 | 12 | cd my_rails_app 13 | ./script/generate papermill_assets 14 | -------------------------------------------------------------------------------- /app/views/papermill/_thumbnail_asset.html.erb: -------------------------------------------------------------------------------- 1 | <% if thumbnail_asset.image? %> 2 | <%= image_tag(thumbnail_asset.url(thumbnail_style)) %> 3 | <% else %> 4 | <%= truncate(thumbnail_asset.name, :length => 15) %> 5 | <%= thumbnail_asset.content_type %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | map.resources :papermill, :collection => { :mass_edit => :post, :add_list => :post, :browser => :get }, :member => { :crop => :get } 3 | map.connect "#{Papermill::options[:papermill_url_prefix]}/#{Papermill::compute_paperclip_path.gsub(":id_partition", ":id0/:id1/:id2")}", :controller => "papermill", :action => "show", :requirements => { :style => /.*/ } 4 | end -------------------------------------------------------------------------------- /generators/papermill_assets/papermill_assets_generator.rb: -------------------------------------------------------------------------------- 1 | class PapermillAssetsGenerator < Rails::Generator::Base 2 | def initialize(args, options = {}) 3 | end 4 | 5 | def manifest 6 | puts "Copying papermill assets to your public directory..." 7 | FileUtils.cp_r( 8 | Dir[File.join(File.dirname(__FILE__), '../../public')], 9 | File.join(RAILS_ROOT) 10 | ) 11 | puts "Done! Check public/ for result." 12 | exit 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/papermill/flash_session_cookie_middleware.rb: -------------------------------------------------------------------------------- 1 | require 'rack/utils' 2 | 3 | class FlashSessionCookieMiddleware 4 | def initialize(app, session_key = '_session_id') 5 | @app = app 6 | @session_key = session_key 7 | end 8 | 9 | def call(env) 10 | if env['HTTP_USER_AGENT'] =~ /^(Adobe|Shockwave) Flash/ 11 | params = ::Rack::Request.new(env).params 12 | env['HTTP_COOKIE'] = [ @session_key, params[@session_key] ].join('=').freeze unless params[@session_key].nil? 13 | end 14 | @app.call(env) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /generators/papermill_initializer/papermill_initializer_generator.rb: -------------------------------------------------------------------------------- 1 | class PapermillInitializerGenerator < Rails::Generator::Base 2 | def initialize(args, options = {}) 3 | end 4 | 5 | def manifest 6 | puts "Copying papermill initializer to config/initializers/..." 7 | FileUtils.rm_rf("#{RAILS_ROOT}/config/initializers/papermill.rb") 8 | FileUtils.cp_r( 9 | File.join(File.dirname(__FILE__), 'papermill_initializer.rb'), 10 | "#{RAILS_ROOT}/config/initializers/papermill.rb" 11 | ) 12 | puts "Done! Check config/initializer/papermill.rb for result." 13 | exit 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /generators/papermill_table/papermill_table_generator.rb: -------------------------------------------------------------------------------- 1 | class PapermillTableGenerator < Rails::Generator::NamedBase 2 | attr_accessor :class_name, :migration_name 3 | 4 | def initialize(args, options = {}) 5 | super 6 | @class_name = args[0] 7 | end 8 | 9 | def manifest 10 | @migration_name = file_name.camelize 11 | 12 | FileUtils.rm_rf("#{File.join(RAILS_ROOT)}/public/papermill/") 13 | FileUtils.cp_r( 14 | Dir[File.join(File.dirname(__FILE__), '../../public')], 15 | File.join(RAILS_ROOT), 16 | :verbose => true 17 | ) 18 | 19 | record do |m| 20 | # Migration creation 21 | m.migration_template "migrate/papermill_migration.rb.erb", "db/migrate", :migration_file_name => migration_name.underscore 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /public/Jcrop/jquery.Jcrop.css: -------------------------------------------------------------------------------- 1 | /* Fixes issue here http://code.google.com/p/jcrop/issues/detail?id=1 */ 2 | .jcrop-holder { text-align: left; } 3 | 4 | .jcrop-vline, .jcrop-hline 5 | { 6 | font-size: 0; 7 | position: absolute; 8 | background: white url('/Jcrop/images/Jcrop.gif') top left repeat; 9 | } 10 | .jcrop-vline { height: 100%; width: 1px !important; } 11 | .jcrop-hline { width: 100%; height: 1px !important; } 12 | .jcrop-handle { 13 | font-size: 1px; 14 | width: 7px !important; 15 | height: 7px !important; 16 | border: 1px #eee solid; 17 | background-color: #333; 18 | *width: 9px; 19 | *height: 9px; 20 | } 21 | 22 | .jcrop-tracker { width: 100%; height: 100%; } 23 | 24 | .custom .jcrop-vline, 25 | .custom .jcrop-hline 26 | { 27 | background: yellow; 28 | } 29 | .custom .jcrop-handle 30 | { 31 | border-color: black; 32 | background-color: #C7BB00; 33 | -moz-border-radius: 3px; 34 | -webkit-border-radius: 3px; 35 | } 36 | -------------------------------------------------------------------------------- /app/views/papermill/_asset.html.erb: -------------------------------------------------------------------------------- 1 | <%- dom_id = "#{field_id}_papermill_asset_#{asset.id}" -%> 2 | <%- delete_link = %{ asset.name)}" src="/papermill/images/delete.png" alt="delete"/>} %> 3 | 4 |
asset.name) %>" onDblClick="popup(jQuery(this).attr('rel')); return false;" rel="<%= edit_papermill_url(asset, :targetted_size => targetted_size) %>"> 5 | <%= delete_link %> 6 | <%- if thumbnail_style -%> 7 | <%= render :partial => "papermill/thumbnail_asset", :object => asset, :locals => {:thumbnail_style => thumbnail_style} %> 8 | <%- else -%> 9 | <%= render :partial => "papermill/raw_asset", :object => asset %> 10 | <%- end -%> 11 | <%= hidden_field_tag field_name, asset.id.to_s, :id => nil %> 12 |
13 | -------------------------------------------------------------------------------- /app/views/papermill/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | <% fields_for :papermill_asset, @asset do |form| %> 5 | <% @asset.papermill_options[:editable_fields].each do |field| %> 6 | <% key = field.keys.first %> 7 |

8 | <%= form.label(key, (field[key][:label] || t("papermill.#{key}")).to_s) %>
9 | <% case field[key][:type] 10 | when "text" %> 11 | <%= form.text_area key %> 12 | <% when "string" %> 13 | <%= form.text_field key %> 14 | <% when "boolean" -%> 15 | <%= form.check_box key %> 16 | <% else %> 17 | <%= form.text_field key %> 18 | <% end -%> 19 |

20 | <% end %> 21 | <% end %> 22 | <%= submit_tag t('papermill.save') %> 23 |
24 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 [Benoit Bénézech] 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /app/views/papermill/browser.html.erb: -------------------------------------------------------------------------------- 1 | <% params.delete(:action) %> 2 |
3 | 19 |
20 | <%= submit_tag "Valider" %> 21 |
-------------------------------------------------------------------------------- /lib/papermill.rb: -------------------------------------------------------------------------------- 1 | require "rbconfig" 2 | require "mime/types" 3 | require "rubygems" 4 | gem "paperclip", "2.3.1.1" 5 | require "paperclip" 6 | 7 | I18n.load_path = [File.join(File.dirname(__FILE__), "../config/locales/papermill.yml")] + I18n.load_path 8 | require 'papermill/extensions' 9 | require 'papermill/flash_session_cookie_middleware.rb' 10 | 11 | Object.send :include, PapermillObjectExtensions 12 | Hash.send :include, PapermillHashExtensions 13 | Array.send :include, PapermillArrayExtensions 14 | String.send :include, StringToUrlNotFound unless String.instance_methods.include? "to_url" 15 | String.send :include, HtmlSafeBackwardCompatibilityFix unless String.instance_methods.include? "html_safe" 16 | Formtastic::SemanticFormBuilder.send(:include, PapermillFormtasticExtensions) rescue NameError 17 | 18 | require 'papermill/papermill_options.rb' 19 | begin 20 | require File.join(RAILS_ROOT, "config/initializers/papermill.rb") 21 | rescue LoadError, MissingSourceFile 22 | end 23 | require 'papermill/papermill_paperclip_processor' 24 | require 'papermill/papermill' 25 | require 'papermill/papermill_association' 26 | require 'papermill/papermill_asset' 27 | require 'papermill/form_builder' 28 | require 'papermill/papermill_helper' 29 | ActionView::Base.send :include, PapermillHelper 30 | ActiveRecord::Base.send :include, Papermill 31 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'rake' 3 | require 'rake/testtask' 4 | require 'rake/rdoctask' 5 | 6 | desc 'Default: run unit tests.' 7 | task :default => :test 8 | 9 | desc 'Test the papermill plugin.' 10 | Rake::TestTask.new(:test) do |t| 11 | t.libs << 'lib' 12 | t.libs << 'test' 13 | t.pattern = 'test/**/*_test.rb' 14 | t.verbose = true 15 | end 16 | 17 | desc 'Generate documentation for the papermill plugin.' 18 | Rake::RDocTask.new(:rdoc) do |rdoc| 19 | rdoc.rdoc_dir = 'rdoc' 20 | rdoc.title = 'Papermill' 21 | rdoc.options << '--line-numbers' << '--inline-source' 22 | rdoc.rdoc_files.include('README') 23 | rdoc.rdoc_files.include('lib/**/*.rb') 24 | end 25 | 26 | begin 27 | require 'jeweler' 28 | Jeweler::Tasks.new do |gemspec| 29 | gemspec.name = "papermill" 30 | gemspec.summary = "Paperclip Swfupload UploadHelper wrapper" 31 | gemspec.description = "Paperclip Swfupload UploadHelper wrapper" 32 | gemspec.email = "benoit.benezech@gmail.com" 33 | gemspec.homepage = "http://github.com/bbenezech/papermill" 34 | gemspec.authors = ["Benoit Bénézech"] 35 | gemspec.add_dependency('paperclip', '2.3.1.1') 36 | gemspec.add_dependency('mime-types') 37 | end 38 | rescue LoadError 39 | puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 40 | end -------------------------------------------------------------------------------- /lib/papermill/extensions.rb: -------------------------------------------------------------------------------- 1 | class PapermillException < Exception; end 2 | 3 | module HtmlSafeBackwardCompatibilityFix 4 | def html_safe 5 | self 6 | end 7 | end 8 | 9 | module PapermillHashExtensions 10 | def deep_merge(hash) 11 | target = dup 12 | hash.keys.each do |key| 13 | if hash[key].is_a? Hash and self[key].is_a? Hash 14 | target[key] = target[key].deep_merge(hash[key]) 15 | next 16 | end 17 | target[key] = hash[key] 18 | end 19 | target 20 | end 21 | end 22 | 23 | module PapermillObjectExtensions 24 | # Nil if empty. 25 | def nie 26 | self.blank? ? nil : self 27 | end 28 | end 29 | 30 | module PapermillArrayExtensions 31 | def map_with_index! 32 | each_with_index do |e, idx| self[idx] = yield(e, idx); end 33 | end 34 | 35 | def map_with_index(&block) 36 | dup.map_with_index!(&block) 37 | end 38 | end 39 | 40 | module PapermillFormtasticExtensions 41 | def image_upload_input(method, options) 42 | self.label(method, options_for_label(options)) + 43 | self.send(:image_upload, method, options) 44 | end 45 | def images_upload_input(method, options) 46 | self.label(method, options_for_label(options)) + 47 | self.send(:images_upload, method, options) 48 | end 49 | def asset_upload_input(method, options) 50 | self.label(method, options_for_label(options)) + 51 | self.send(:asset_upload, method, options) 52 | end 53 | def assets_upload_input(method, options) 54 | self.label(method, options_for_label(options)) + 55 | self.send(:assets_upload, method, options) 56 | end 57 | end 58 | 59 | 60 | module StringToUrlNotFound 61 | def to_url 62 | gsub(/[^a-zA-Z0-9]/, "-").gsub(/-+/, "-").gsub(/^-|-$/, "").downcase 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /public/facebox/facebox.css: -------------------------------------------------------------------------------- 1 | #facebox .b { 2 | background:url(/facebox/b.png); 3 | } 4 | 5 | #facebox .tl { 6 | background:url(/facebox/tl.png); 7 | } 8 | 9 | #facebox .tr { 10 | background:url(/facebox/tr.png); 11 | } 12 | 13 | #facebox .bl { 14 | background:url(/facebox/bl.png); 15 | } 16 | 17 | #facebox .br { 18 | background:url(/facebox/br.png); 19 | } 20 | 21 | #facebox { 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | z-index: 100; 26 | text-align: left; 27 | } 28 | 29 | #facebox .popup { 30 | position: relative; 31 | } 32 | 33 | #facebox table { 34 | border-collapse: collapse; 35 | } 36 | 37 | #facebox td { 38 | border-bottom: 0; 39 | padding: 0; 40 | } 41 | 42 | #facebox .body { 43 | padding: 10px; 44 | background: #fff; 45 | width: 370px; 46 | } 47 | 48 | #facebox .loading { 49 | text-align: center; 50 | } 51 | 52 | #facebox .image { 53 | text-align: center; 54 | } 55 | 56 | #facebox img { 57 | border: 0; 58 | margin: 0; 59 | } 60 | 61 | #facebox .footer { 62 | border-top: 1px solid #DDDDDD; 63 | padding-top: 5px; 64 | margin-top: 10px; 65 | text-align: right; 66 | } 67 | 68 | #facebox .tl, #facebox .tr, #facebox .bl, #facebox .br { 69 | height: 10px; 70 | width: 10px; 71 | overflow: hidden; 72 | padding: 0; 73 | } 74 | 75 | #facebox_overlay { 76 | position: fixed; 77 | top: 0px; 78 | left: 0px; 79 | height:100%; 80 | width:100%; 81 | } 82 | 83 | .facebox_hide { 84 | z-index:-100; 85 | } 86 | 87 | .facebox_overlayBG { 88 | background-color: #000; 89 | z-index: 99; 90 | } 91 | 92 | * html #facebox_overlay { /* ie6 hack */ 93 | position: absolute; 94 | height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px'); 95 | } 96 | -------------------------------------------------------------------------------- /app/views/papermill/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <% if @asset.image? %> 4 | <%= image_tag(@asset.url("400x400>"), :onDblClick => "popup('#{crop_papermill_path(@asset, :target => "original", :targetted_size => params[:targetted_size])}'); return false;", :title => I18n.t("papermill.crop", :resource => @asset.name)) %> 5 |
6 | <% else %> 7 | <%= link_to t("file_type", :type => @asset.content_type, :scope => 'papermill'), @asset.url, :popup => true %> 8 | <% end -%> 9 |
10 | 28 | 29 |
30 | 33 |
34 | -------------------------------------------------------------------------------- /lib/papermill/papermill_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module PapermillHelper 4 | 5 | # Sets all the javascript needed for papermill. 6 | # If jQuery and JQueryUI (with Sortable) are already loaded, call papermill_javascript_tag 7 | # If you use some other JS Framework, call papermill_javascript_tag(:with_jquery => "no_conflict") 8 | # If you want to rely on this helper to load jQuery and JQueryUI and use them after, call papermill_javascript_tag(:with_jquery => true) 9 | 10 | def papermill_javascript_tag(options = {}) 11 | html = [] 12 | html << %{\ 13 | } if options[:with_jquery] 14 | html << %{} 17 | html << javascript_include_tag("/facebox/facebox.js", "/jgrowl/jquery.jgrowl_minimized.js", "/Jcrop/jquery.Jcrop.min.js", "/swfupload/swfupload.js", "/papermill/papermill.js", :cache => "papermill") 18 | unless @content_for_papermill_inline_js.blank? 19 | html << %{} 22 | end 23 | html.join("\n") 24 | end 25 | 26 | # Sets the css tags needed for papermill. 27 | def papermill_stylesheet_tag(options = {}) 28 | html = [] 29 | html << stylesheet_link_tag("/facebox/facebox.css", "/jgrowl/jquery.jgrowl.css", "/Jcrop/jquery.Jcrop.css", "/papermill/papermill.css", :cache => "papermill") 30 | unless @content_for_papermill_inline_css.blank? 31 | html << %{} 34 | end 35 | html.join("\n") 36 | end 37 | end -------------------------------------------------------------------------------- /config/locales/papermill.yml: -------------------------------------------------------------------------------- 1 | en: 2 | papermill: 3 | not-processed: "Error/resource not processed" 4 | updated: "'{{resource}}' updated" 5 | not-found: "'{{resource}}' not found" 6 | edit-title: "Click to edit '{{resource}}'" 7 | crop: "Double-click to crop '{{resource}}'" 8 | edit-with-pixlr: "Edit '{{resource}}' online @ pixlr.com" 9 | thumbnail-edit-title: "Double-click to edit '{{resource}}'" 10 | upload-button-wording: "Upload..." 11 | delete: "Remove {{resource}}" 12 | delete-confirmation: "Delete '{{resource}}'?" 13 | file_type: "{{type}} file" 14 | name: "Name" 15 | content_type: "Content-type" 16 | size: "File size" 17 | class_name: "Asset type" 18 | created_at: "Created at" 19 | updated_at: "Updated at" 20 | title: "Title" 21 | copyright: "Copyright" 22 | alt: "Alternative text" 23 | description: "Description" 24 | width: "Width" 25 | height: "Height" 26 | save: "Save" 27 | back: "Back" 28 | url: "URL : " 29 | modify-all: "Modify all: " 30 | delete-all: "Delete all" 31 | delete-all-confirmation: "Are you sure?" 32 | mass-thumbnail-reset: "Delete all thumbnails" 33 | mass-thumbnail-reset-confirmation: "Are you sure?" 34 | fr: 35 | papermill: 36 | not-processed: "Erreur/ressource non trouvée" 37 | updated: "'{{resource}}' modifiés(s)" 38 | not-found: "'{{resource}}' non trouvé" 39 | edit-title: "Cliquer pour éditer '{{resource}}'" 40 | crop: "Double-cliquer pour retailler '{{resource}}'" 41 | edit-with-pixlr: "Éditer '{{resource}}' en-ligne sur pixlr.com" 42 | thumbnail-edit-title: "Double-cliquer pour éditer '{{resource}}'" 43 | upload-button-wording: "Charger.." 44 | delete: "Supprimer {{resource}}" 45 | delete-confirmation: "Êtes-vous sûr de vouloir supprimer '{{resource}}' ?" 46 | file_type: "Fichier {{type}}" 47 | name: "Nom" 48 | content_type: "Content-type" 49 | size: "Taille du fichier" 50 | class_name: "Type d'asset" 51 | created_at: "Création" 52 | updated_at: "Mise à jour" 53 | title: "Titre" 54 | copyright: "Copyright" 55 | alt: "Texte alternatif" 56 | description: "Description" 57 | width: "Largeur" 58 | height: "Hauteur" 59 | save: "Modifier" 60 | back: "Retour" 61 | url: "Lien : " 62 | modify-all: "Modifier tout : " 63 | delete-all: "Supprimer tout" 64 | delete-all-confirmation: "Êtes-vous sûr ?" 65 | mass-thumbnail-reset: "Supprimer tous les thumbnails" 66 | mass-thumbnail-reset-confirmation: "Êtes-vous sûr ?" 67 | -------------------------------------------------------------------------------- /lib/papermill/papermill_options.rb: -------------------------------------------------------------------------------- 1 | module Papermill 2 | BASE_OPTIONS = { 3 | :class_name => "PapermillAsset", 4 | :through => false, 5 | :inline_css => true, 6 | :use_content_for => true, 7 | :images_only => false, 8 | :form_helper_elements => [:upload_button, :container, :browser, :mass_edit], 9 | :mass_edit => true, 10 | :mass_editable_fields => ["title", "copyright", "description"], 11 | :editable_fields => [ 12 | {:title => {:type => "string"}}, 13 | {:alt => {:type => "string"}}, 14 | {:copyright => {:type => "string"}}, 15 | {:description => {:type => "text" }} 16 | ], 17 | :targetted_size => nil, 18 | :gallery => { 19 | :width => nil, 20 | :height => nil, 21 | :columns => 8, 22 | :lines => 2, 23 | :vpadding => 0, 24 | :hpadding => 0, 25 | :vmargin => 1, 26 | :hmargin => 1, 27 | :border_thickness => 2 28 | }, 29 | :thumbnail => { 30 | :width => 100, 31 | :height => 100, 32 | :aspect_ratio => nil, 33 | :style => nil 34 | }, 35 | :swfupload => { 36 | :flash_url => "'/swfupload/swfupload.swf'", 37 | :button_image_url => "'/papermill/images/upload-blank.png'", 38 | :button_width => 61, 39 | :button_height => 22, 40 | :button_text => %{'#{I18n.t("papermill.upload-button-wording")}'}, 41 | :button_text_style => %{'.button-text { font-size: 12pt; font-weight: bold; }'}, 42 | :button_text_top_padding => 4, 43 | :button_text_left_padding => 4, 44 | :debug => false, 45 | :prevent_swf_caching => true, 46 | :file_size_limit => "'10 MB'" 47 | }, 48 | :copyright => nil, 49 | :copyright_text_transform => Proc.new {|c| c }, 50 | :copyright_im_command => %{\\( -font Arial-Bold -pointsize 9 -fill '#FFFFFFE0' -border 3 -bordercolor '#50550080' -background '#00000000' label:' %s ' \\) -gravity South-West -geometry +0+0 -composite}, 51 | :watermark => "/images/rails.png", 52 | :watermark_im_command => %{- | composite \\( %s -resize 100% \\) - -dissolve 20% -gravity center -geometry +0+0 }, 53 | :alias_only => false, 54 | :aliases => {}, 55 | :use_url_key => false, 56 | :url_key_salt => "change-me-please", 57 | :url_key_generator => Proc.new { |style, asset| Digest::SHA512.hexdigest("#{style}#{asset.id}#{Papermill::options[:url_key_salt]}")[0..10] }, 58 | :use_id_partition => true, 59 | :papermill_url_prefix => "/system/papermill", 60 | :papermill_path_prefix => ":rails_root/public/system/papermill" 61 | } 62 | end 63 | -------------------------------------------------------------------------------- /generators/papermill_table/templates/migrate/papermill_migration.rb.erb: -------------------------------------------------------------------------------- 1 | class <%= migration_name %> < ActiveRecord::Migration 2 | def self.up 3 | create_table :papermill_assets do |t| 4 | 5 | # Paperclip fields (required) 6 | t.string :file_file_name 7 | t.string :file_content_type 8 | t.integer :file_file_size 9 | 10 | # Papermill fields (required) 11 | t.integer :position # sets are ordered by position 12 | 13 | t.belongs_to :assetable, :polymorphic => true 14 | t.string :assetable_key 15 | t.string :type # PapermillAsset STI 16 | t.string :title # filename not transformed, without file extension, for your own use 17 | 18 | # Papermill magical fields (You'll need to configure :mass_editable_fields/:editable_fields accordingly to be able to modify them with Papermill helpers) 19 | 20 | t.string :copyright # copyright content 21 | t.string :copyright_im_command # copyright ImageMagick command 22 | t.string :watermark # watermark URI 23 | t.string :watermark_im_command # watermark ImageMagick content 24 | 25 | # Example additionals fields (configure :mass_editable_fields/:editable_fields accordingly to be able to modify them with Papermill helpers) 26 | 27 | t.string :alt 28 | t.text :description 29 | t.timestamps 30 | end 31 | 32 | change_table :papermill_assets do |t| 33 | t.index [:assetable_id, :assetable_type, :assetable_key, :position], { :name => "papermill_assets_index" } 34 | t.index [:assetable_key, :position] # for non assetable assets 35 | end 36 | 37 | # If you want to associate an asset to more than one assetable, use the (:through => true) option to use the join-table and get an assetable with smtg like 38 | # has_many :assets, :through => :papermill_associations 39 | 40 | create_table :papermill_associations, :force => true do |t| 41 | t.belongs_to :assetable, :polymorphic => true 42 | t.string :assetable_key 43 | t.integer :position 44 | t.belongs_to :papermill_asset 45 | t.timestamps 46 | end 47 | 48 | change_table :papermill_associations do |t| 49 | t.index [:assetable_id, :assetable_type, :assetable_key, :position], { :name => "papermill_associations_index" } 50 | t.index :papermill_asset_id 51 | end 52 | 53 | end 54 | 55 | def self.down 56 | drop_table :papermill_assets 57 | drop_table :papermill_associations 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/views/papermill/crop.html.erb: -------------------------------------------------------------------------------- 1 | <%= image_tag(@asset.url, :id => "cropbox") %> 2 | 3 | 24 | 25 | <% 26 | unless (@size = params[:targetted_size]).blank? 27 | @target_w, @target_h = @size.split("x").map(&:to_i) 28 | @init_x = (@asset.width - @target_w.to_i) / 2 29 | @init_y = (@asset.height - @target_h.to_i) / 2 30 | end 31 | %> 32 | 33 | 34 |
35 |
36 | 37 | 38 | <% fields_for :papermill_asset, @asset do |f| %> 39 | 40 | 41 | <%= f.hidden_field :crop_x, :id => :crop_x %> 42 | <%= f.hidden_field :crop_y, :id => :crop_y %> 43 | <%= f.submit I18n.t('papermill.save'), :class => "submit" %> 44 | <%= link_to_function I18n.t("papermill.back"), "popup('#{edit_papermill_path(@asset, :targetted_size => params[:targetted_size])}')" %> 45 | <% end -%> 46 |
47 |
48 | 49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /public/papermill/papermill.css: -------------------------------------------------------------------------------- 1 | #papermill-box { width:862px; margin-bottom:10px; } 2 | #papermill-box #left { float:left; width:400px; padding-bottom:10px; } 3 | #papermill-box #right { float:right; width:420px; margin-left:20px; border-left:2px solid GREY; padding-left:20px; padding-bottom:10px; } 4 | #papermill-box input.text_field { width:400px; margin:5px 0; } 5 | #papermill-box textarea { width:403px; height:100px; margin:5px 0; } 6 | #papermill-box label { } 7 | #papermill-box p { padding: 3px 0; margin:0;} 8 | 9 | #papermill-box #footer { text-align:center; font-size:10px; padding:5px } 10 | #papermill-box #read-write { margin:10px 0 0 5px; } 11 | 12 | #papermill-box #error { color:red; padding:10px; margin:10px; border:1px dashed green; background-color:#DDD; } 13 | 14 | #papermill-box .right-cell {} 15 | #papermill-box .left-cell {font-weight:bold; padding-right:10px; text-align:right;} 16 | 17 | .papermill { overflow:hidden; } 18 | .papermill a img { border:0px; } 19 | .papermill .asset:hover { border-color:blue; } 20 | .papermill .asset a { display:block; } 21 | .papermill .progress { display:block; border:1px solid #C2E3EF; text-align:left; height:6px; } 22 | .papermill .progress span { background:#7BB963; height:6px; width:0; display:block; } 23 | 24 | .papermill .dashboard li a { display:inline; padding-left:20px } 25 | .papermill .dashboard li.mass_edit a { background:transparent url(/papermill/images/mass-edit.png) no-repeat top left; } 26 | 27 | .papermill-thumb-container { position:relative; border:5px solid #EEE; padding:4px; overflow:hidden; } 28 | .papermill-thumb-container .asset { border:0px solid transparent; min-height:25px; min-width:25px; display:block; float:left; position:relative; } 29 | .papermill-thumb-container .asset span { display:block; } 30 | .papermill-thumb-container .asset .delete { position:absolute; bottom:5px; right:5px; } 31 | .papermill-thumb-container .asset .name { font-size:10px; overflow:hidden; font-weight:bold; } 32 | .papermill-thumb-container .asset .infos { font-size:8px; overflow:hidden; } 33 | .papermill-thumb-container .asset .status { margin-bottom:10px; } 34 | 35 | .papermill-asset-container { border:1px solid #EEE; padding:3px; } 36 | .papermill-asset-container .asset { display:block; height:22px; } 37 | .papermill-asset-container .asset .swfupload .name { margin-left:21px; } 38 | .papermill-asset-container .asset .name { float:left; } 39 | .papermill-asset-container .asset .status { margin-left:5px; float:left; } 40 | .papermill-asset-container .asset .progress { float:left; margin-top:6px; margin-left:5px; width:100px; } 41 | .papermill-asset-container .asset .delete { float:left; margin-top:2px; margin-right:5px; } 42 | 43 | .papermill-thumb-container.papermill-multiple-items .asset { cursor:move; } 44 | 45 | .papermill-asset-container.papermill-multiple-items { padding-left:10px; border-left:5px solid #EEE; min-height:44px; } 46 | .papermill-asset-container.papermill-multiple-items .asset { cursor:row-resize; } 47 | 48 | .papermill-asset-container.papermill-unique-item { padding-left:5px; min-height:22px; } 49 | 50 | /* Need some backgrounds? 51 | .papermill .asset { background:transparent url(/papermill/images/background.png) repeat top left; } 52 | .papermill-thumb-container { background:transparent url(/papermill/images/container-background.jpg) repeat top left; } 53 | */ 54 | -------------------------------------------------------------------------------- /lib/papermill/papermill_paperclip_processor.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Paperclip 4 | 5 | # Handles thumbnailing images that are uploaded. 6 | class PapermillPaperclipProcessor < Thumbnail 7 | 8 | attr_accessor :crop_h, :crop_w, :crop_x, :crop_y, :copyright, :watermark_path 9 | 10 | def initialize(file, options = {}, attachment = nil) 11 | @crop_h, @crop_w, @crop_x, @crop_y = options[:crop_h], options[:crop_w], options[:crop_x], options[:crop_y] 12 | 13 | # copyright extraction 14 | if options[:geometry] =~ /©/ || options[:copyright] 15 | options[:geometry], *@copyright = options[:geometry].split("©", -1) 16 | 17 | @copyright = ( 18 | (options[:copyright] != true && options[:copyright]) || 19 | @copyright.join("©").nie || 20 | file.instance.respond_to?(:copyright) && file.instance.copyright.nie || 21 | file.instance.papermill_options[:copyright].nie) 22 | 23 | if @copyright 24 | @copyright = (options[:copyright_text_transform] || file.instance.papermill_options[:copyright_text_transform]).try(:call, @copyright) || @copyright 25 | end 26 | end 27 | 28 | # watermark extraction 29 | if options[:watermark] || options[:geometry] =~ /\-wm/ 30 | options[:geometry] = options[:geometry].chomp("-wm") 31 | @watermark_path = options[:watermark].is_a?(String) && options[:watermark] || file.instance.papermill_options[:watermark] 32 | @watermark_path = "#{RAILS_ROOT}/public" + @watermark_path if @watermark_path.starts_with?("/") 33 | end 34 | 35 | 36 | if options[:geometry] =~ /#.+/ 37 | # let's parse : 38 | # x#x:: 39 | # x#x 40 | # x#: 41 | 42 | options[:geometry], manual_crop = options[:geometry].split("#") 43 | crop_dimensions, @crop_x, @crop_y = manual_crop.split("+") 44 | 45 | if crop_dimensions =~ /x/ 46 | @crop_w, @crop_h = crop_dimensions.split("x") 47 | else 48 | @crop_x, @crop_y = crop_dimensions, @crop_x 49 | end 50 | 51 | options[:geometry] = (options[:geometry].nie || "#{@crop_x}x#{@crop_y}") + "#" 52 | 53 | unless @crop_w && @crop_h 54 | @target_geometry = Geometry.parse(options[:geometry]) 55 | @crop_w ||= @target_geometry.try(:width).to_i 56 | @crop_h ||= @target_geometry.try(:height).to_i 57 | end 58 | end 59 | super 60 | end 61 | 62 | def transformation_command 63 | " -limit memory 1 -limit map 1 #{(crop_command ? super.sub(/ -crop \S+/, crop_command) : super)} #{copyright_command} #{watermark_command}".sub(%{-resize "0x" }, "") 64 | end 65 | 66 | def copyright_command 67 | (options[:copyright_im_command] || @file.instance.papermill_options[:copyright_im_command]).gsub(/%s/, @copyright.gsub(/'/, %{'"'"'}).sub("-slash-", "/").sub("-backslash-", %{\\\\\\})) if @copyright 68 | end 69 | 70 | def watermark_command 71 | (options[:watermark_im_command] || @file.instance.papermill_options[:watermark_im_command]).gsub(/%s/, @watermark_path) if @watermark_path 72 | end 73 | 74 | def crop_command 75 | if @crop_h || @crop_x 76 | " -crop '%dx%d+%d+%d'" % [ @crop_w, @crop_h, @crop_x, @crop_y ].map(&:to_i) 77 | end 78 | end 79 | end 80 | 81 | 82 | end 83 | -------------------------------------------------------------------------------- /demo.txt: -------------------------------------------------------------------------------- 1 | gem 'papermill' 2 | 3 | generate :papermill_table, "PapermillMigration" 4 | generate :papermill_assets 5 | generate :papermill_initializer 6 | generate :scaffold, "article title:string" 7 | rake "db:migrate" 8 | 9 | file "app/models/article.rb", <<-END 10 | class Article < ActiveRecord::Base 11 | validates_presence_of :title 12 | papermill :image_gallery, :thumbnail => {:width => 75, :height => 100}, :images_only => true # only images, custom preview thumbnail size 13 | papermill :thumbnail, :swfupload => { :file_types => "*.jpg;*.jpeg" } # jpg only, cherry picking 14 | papermill :my_assets 15 | papermill :my_other_asset 16 | end 17 | END 18 | 19 | 20 | file "app/views/articles/edit.html.erb", <<-END 21 |

Editing article

22 | <%= render :partial => "form" %> 23 | <%= link_to 'Show', @article %> | 24 | <%= link_to 'Back', articles_path %> 25 | END 26 | 27 | file "app/views/articles/new.html.erb", <<-END 28 |

New article

29 | <%= render :partial => "form" %> 30 | <%= link_to 'Back', articles_path %> 31 | END 32 | 33 | file "app/views/articles/_form.html.erb", <<-END 34 | <% form_for(@article) do |f| %> 35 | <%= f.error_messages %>

36 | <%= f.label :title %>
37 | <%= f.text_field :title %>

38 | <%= f.label :image_gallery %>
39 | <%= f.images_upload(:image_gallery) %>

40 | <%= f.label :thumbnail %>
41 | <%= f.image_upload(:thumbnail) %>

42 | <%= f.label :my_assets %>
43 | <%= f.assets_upload(:my_assets) %>

44 | <%= f.label :my_other_asset %>
45 | <%= f.asset_upload(:my_other_asset) %>

46 | <%= f.submit 'Send' %> 47 | <% end %> 48 | END 49 | 50 | file "app/views/articles/show.html.erb", <<-END 51 |

52 | Title: 53 | <%=h @article.title %> 54 |

55 |

56 | @article.image_gallery.each : 57 |

58 | <% @article.image_gallery.each do |image| %> 59 | <%= link_to(image_tag(image.url("100x100#")), image.url) %> 60 | <% end %> 61 |

62 |

63 | @article.thumbnail.first : 64 |

65 | <% image = @article.thumbnail.first %> 66 | <%= link_to(image_tag(image.url("100x100#")), image.url) if image %> 67 |

68 |

69 | @article.my_assets.each : 70 |

71 |

    72 | <% @article.my_assets.each do |asset| %> 73 |
  • <%= link_to asset.name, asset.url %>
  • 74 | <% end %> 75 |
76 |

77 |

78 | @article.my_other_asset.first : 79 |

80 | <% asset = @article.my_other_asset.first %> 81 | <%= link_to(asset.name, asset.url) if asset %> 82 |

83 | 84 | <%= link_to 'Edit', edit_article_path(@article) %> | 85 | <%= link_to 'Back', articles_path %> 86 | END 87 | 88 | 89 | file "app/views/layouts/application.html.erb", <<-END 90 | 92 | 93 | 94 | 95 | 96 | Papermill Demo 97 | <%= stylesheet_link_tag 'scaffold' %> 98 | <%= papermill_stylesheet_tag %> 99 | 100 | 101 | <%= yield %> 102 | <%= papermill_javascript_tag :with_jquery => true %> 103 | 104 | 105 | END 106 | 107 | run "rm app/views/layouts/articles.html.erb" 108 | run "rm public/index.html" 109 | route "map.root :controller => 'articles'" 110 | -------------------------------------------------------------------------------- /test/papermill_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | require 'rubygems' 4 | gem 'rails' 5 | require 'active_record' 6 | require 'action_controller' 7 | require 'action_view' 8 | require 'active_support' 9 | ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/info.log") 10 | RAILS_ROOT = File.join( File.dirname(__FILE__), "../../../.." ) 11 | 12 | module Papermill 13 | OPTIONS = { 14 | :aliases => { 15 | 'tall' => :"1000x1000", 16 | :small => "100x100", 17 | :hashed_style => {:geometry => "100x100"} 18 | } 19 | } 20 | end 21 | 22 | require File.join(File.dirname(__FILE__), '../init.rb') 23 | 24 | ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => "test.sqlite3") 25 | ActiveRecord::Schema.define(:version => 1) do 26 | create_table :papermill_assets, :force => true do |t| 27 | t.string :title, :file_file_name, :file_content_type, :assetable_type, :assetable_key, :type 28 | t.integer :file_file_size, :position, :assetable_id 29 | t.timestamps 30 | end 31 | 32 | create_table :articles, :force => true do |t| 33 | t.string :type 34 | end 35 | end 36 | 37 | class MyAsset < PapermillAsset 38 | end 39 | 40 | class Article < ActiveRecord::Base 41 | def self.table_name 42 | :articles 43 | end 44 | papermill :asset1 45 | papermill :my_assets, :class_name => "MyAsset" 46 | end 47 | 48 | class PapermillTest < Test::Unit::TestCase 49 | @article = Article.create! 50 | @decoy_article = Article.create! 51 | @file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb') 52 | PapermillAsset.create!(:file => @file, :assetable => @article, :assetable_key => "asset1", :position => 2) 53 | PapermillAsset.create!(:file => @file, :assetable => @article, :assetable_key => "asset1", :position => 1) 54 | MyAsset.create!(:file => @file, :assetable => @article, :assetable_key => "my_assets") 55 | MyAsset.create!(:file => @file, :assetable => @article, :assetable_key => "my_assets") 56 | MyAsset.create!(:file => @file, :assetable => @decoy_article, :assetable_key => "my_assets") 57 | 58 | def initialize(*args) 59 | super 60 | @file = File.new(File.join(File.dirname(__FILE__), "fixtures", "5k.png"), 'rb') 61 | @article = Article.find 1 62 | @decoy_article = Article.find 2 63 | @asset1 = PapermillAsset.find 1 64 | @asset2 = PapermillAsset.find 2 65 | @asset3 = PapermillAsset.find 3 66 | @asset4 = PapermillAsset.find 4 67 | @asset5 = PapermillAsset.find 5 68 | end 69 | 70 | def test_namedscopes_for_specific_associations 71 | assert_equal @article.my_assets.map(&:id), [3,4] 72 | end 73 | 74 | def test_namedscope_for_global_associations 75 | assert_equal @article.asset1.map(&:id), [2,1] 76 | end 77 | 78 | def test_id_partition 79 | assert_equal @asset1.id_partition, "000/000/001" 80 | end 81 | 82 | def test_name 83 | assert_equal @asset1.name, "5k.png" 84 | end 85 | 86 | def test_width 87 | assert_equal @asset1.width, 434.0 88 | end 89 | 90 | def test_height 91 | assert_equal @asset1.height, 66.0 92 | end 93 | 94 | def test_size 95 | assert_equal @asset1.size, 4456 96 | end 97 | 98 | def test_content_type 99 | assert_equal @asset1.content_type, "image/png" 100 | end 101 | 102 | def test_is_image 103 | assert @asset1.image? 104 | end 105 | 106 | def test_compute_style 107 | assert_equal PapermillAsset.compute_style(:tall), :geometry => "1000x1000" 108 | assert_equal PapermillAsset.compute_style("tall"), :geometry => "1000x1000" 109 | assert_equal PapermillAsset.compute_style(:small), :geometry => "100x100" 110 | assert_equal PapermillAsset.compute_style("small"), :geometry => "100x100" 111 | assert_equal PapermillAsset.compute_style("hashed_style"), :geometry => "100x100" 112 | assert_equal PapermillAsset.compute_style("100x100"), :geometry => "100x100" 113 | assert_equal PapermillAsset.compute_style(:"100x100"), :geometry => "100x100" 114 | Papermill::options[:alias_only] = true 115 | assert_equal PapermillAsset.compute_style("100x100"), false 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /app/controllers/papermill_controller.rb: -------------------------------------------------------------------------------- 1 | class PapermillController < ApplicationController 2 | unloadable 3 | prepend_before_filter :load_asset, :only => [ "show", "destroy", "update", "edit", "crop" ] 4 | skip_before_filter :verify_authenticity_token, :only => [:create] # not needed (Flash same origin policy) 5 | 6 | def show 7 | # first escaping is done by rails prior to route recognition, need to do a second one on MSWIN systems to get original one. 8 | params[:style] = CGI::unescape(params[:style]) if Papermill::MSWIN 9 | if @asset.has_valid_url_key?(params[:url_key], params[:style]) && @asset.create_thumb_file(params[:style]) 10 | redirect_to @asset.url(params[:style]) 11 | else 12 | render :nothing => true, :status => 404 13 | end 14 | end 15 | 16 | def create 17 | @asset = params[:asset_class].constantize.new(params.reject{|k, v| !(PapermillAsset.columns.map(&:name)+["Filedata", "Filename"]).include?(k)}) 18 | if @asset.save 19 | output = render_to_string(:partial => "papermill/asset", :object => @asset, :locals => { :gallery => params[:gallery], :thumbnail_style => params[:thumbnail_style], :targetted_size => params[:targetted_size], :field_name => params[:field_name], :field_id => params[:field_id] }) 20 | render :update do |page| 21 | page << %{ jQuery('##{params[:Fileid]}').replaceWith('#{escape_javascript output}') } 22 | page << %{ jQuery('##{params[:Oldfileid]}').remove() } if params[:Oldfileid] 23 | end 24 | else 25 | render :update do |page| 26 | page << %{ notify('#{@asset.name}', '#{escape_javascript @asset.errors.full_messages.join("
")}', 'error') } 27 | page << %{ jQuery('##{params[:Fileid]}').remove() } 28 | page << %{ jQuery('##{params[:Oldfileid]}').show() } if params[:Oldfileid] 29 | end 30 | end 31 | end 32 | 33 | def edit 34 | render :action => "edit", :layout => false 35 | end 36 | 37 | def crop 38 | render :action => "crop", :layout => false 39 | end 40 | 41 | def update 42 | @asset.create_thumb_file(params[:target], params[:papermill_asset].merge({ :geometry => "#{params[:target]}#" })) if params[:target] 43 | render :update do |page| 44 | if @asset.update_attributes(params[:papermill_asset]) 45 | page << %{ notify('#{@asset.name}', '#{ escape_javascript t("papermill.updated", :resource => @asset.name)}', 'notice'); close_popup(self); } 46 | else 47 | page << %{ jQuery('#error').html('#{ escape_javascript @asset.errors.full_messages.join("
") }'); jQuery('#error').show(); } 48 | end 49 | end 50 | end 51 | 52 | def add_list 53 | @assets = PapermillAsset.find(params[:ids]).sort_by{|asset|params[:ids].index(asset.id.to_s)} unless params[:ids].blank? 54 | output = render_to_string(:partial => "papermill/asset", :collection => (@assets || []), :locals => { :gallery => params[:gallery], :thumbnail_style => params[:thumbnail_style], :targetted_size => params[:targetted_size], :field_name => params[:field_name], :field_id => params[:field_id] }) 55 | render :update do |page| 56 | page << %{ close_popup(); jQuery('##{params[:field_id]}').html('#{escape_javascript output}'); } 57 | end 58 | end 59 | 60 | def browser 61 | ids_list = params[params[:field_id] + "_papermill_asset"] 62 | @selected_assets = ids_list.blank? ? [] : params[:asset_class].constantize.find(ids_list).sort_by{|asset| ids_list.index(asset.id.to_s)} 63 | @other_assets = params[:asset_class].constantize.all - @selected_assets 64 | render :action => "browser", :layout => false 65 | end 66 | 67 | def mass_edit 68 | @assets = (params[(params[:list_id] + "_papermill_asset").to_sym] || []).map{ |id| PapermillAsset.find(id, :include => "assetable") } 69 | @assets.each { |asset| asset.update_attribute(params[:attribute], params[:value]) } 70 | render :update do |page| 71 | page << %{ notify("", "#{ escape_javascript t("papermill.updated", :resource => @assets.map(&:name).to_sentence) }", "notice"); } unless @assets.blank? 72 | end 73 | end 74 | 75 | private 76 | 77 | def load_asset 78 | @asset = PapermillAsset.find(params[:id] || (params[:id0] + params[:id1] + params[:id2]).to_i) 79 | end 80 | end -------------------------------------------------------------------------------- /lib/papermill/papermill.rb: -------------------------------------------------------------------------------- 1 | module Papermill 2 | MSWIN = (Config::CONFIG['host_os'] =~ /mswin|mingw/) 3 | 4 | def self.options 5 | @options ||= BASE_OPTIONS.deep_merge(defined?(OPTIONS) ? OPTIONS : {}) 6 | end 7 | 8 | def self.compute_paperclip_path 9 | path = [] 10 | path << (options[:use_id_partition] ? ":id_partition" : ":id") 11 | path << (":url_key" if options[:use_url_key]) 12 | path << ":style" 13 | path << ":basename.:extension" 14 | path.compact.join("/") 15 | end 16 | 17 | def self.included(base) 18 | base.extend(ClassMethods) 19 | base.class_eval do 20 | def papermill(key, through = ((po = self.class.papermill_options) && (po[key.to_sym] || po[:default]) || Papermill::options)[:through]) 21 | PapermillAsset.papermill(self.class.base_class.name, self.id, key.to_s, through) 22 | end 23 | 24 | def respond_to_with_papermill?(method, *args, &block) 25 | respond_to_without_papermill?(method, *args, &block) || (method.to_s =~ /^papermill_.+_ids=$/) == 0 26 | end 27 | 28 | def method_missing_with_papermill(method, *args, &block) 29 | if method.to_s =~ /^papermill_.+_ids=$/ 30 | self.class.papermill(method.to_s[10..-6]) 31 | self.send(method, *args, &block) 32 | else 33 | method_missing_without_papermill(method, *args, &block) 34 | end 35 | end 36 | end 37 | base.send :alias_method_chain, :method_missing, :papermill 38 | base.send :alias_method_chain, :respond_to?, :papermill 39 | ActionController::Dispatcher.middleware.insert_before(ActionController::Base.session_store, FlashSessionCookieMiddleware, ActionController::Base.session_options[:key]) unless ActionController::Dispatcher.middleware.include?(FlashSessionCookieMiddleware) 40 | end 41 | 42 | module ClassMethods 43 | attr_reader :papermill_options 44 | 45 | def inherited(subclass) 46 | subclass.instance_variable_set("@papermill_options", @papermill_options) 47 | super 48 | end 49 | 50 | def papermill(assoc_key, assoc_options = (@papermill_options && @papermill_options[:default] || {})) 51 | return if @papermill_options && @papermill_options[assoc_key.to_sym] # already defined 52 | raise PapermillException.new("Can't use '#{assoc_key}' association : #{self.name} instances already responds to it !") if self.new.respond_to?(assoc_key) 53 | (@papermill_options ||= {}).merge!( { assoc_key.to_sym => Papermill::options.deep_merge(assoc_options) } ) 54 | return if assoc_key.to_sym == :default 55 | unless papermill_options[assoc_key.to_sym][:through] 56 | self.class_eval %{ 57 | has_many :#{assoc_key}, :as => "assetable", :dependent => :destroy, :order => :position, :class_name => "PapermillAsset", :conditions => {:assetable_key => '#{assoc_key}'}, :before_add => Proc.new{|a, asset| asset.assetable_key = '#{assoc_key}'} 58 | def papermill_#{assoc_key}_ids=(ids) 59 | unless (assets_ids = ids.map(&:to_i).select{|i|i>0}) == self.#{assoc_key}.map(&:id) 60 | assets = PapermillAsset.find(assets_ids) 61 | self.#{assoc_key} = assets_ids.map{|asset_id| assets.select{|asset|asset.id==asset_id}.first} 62 | PapermillAsset.update_all("position = CASE id " + assets_ids.map_with_index{|asset_id, index| " WHEN " + asset_id.to_s + " THEN " + (index+1).to_s }.join + " END", 63 | :id => assets_ids) unless assets_ids.empty? 64 | end 65 | end 66 | } 67 | else 68 | self.class_eval %{ 69 | has_many(:#{assoc_key}_associations, :as => "assetable", :class_name => "PapermillAssociation", :include => :papermill_asset, :dependent => :delete_all, :order => :position, :conditions => {:assetable_key => '#{assoc_key}'}, :before_add => Proc.new{|a, assoc| assoc.assetable_key = '#{assoc_key}'}) 70 | has_many(:#{assoc_key}, :through => :#{assoc_key}_associations, :source => :papermill_asset) 71 | def papermill_#{assoc_key}_ids=(ids) 72 | unless (assets_ids = ids.map(&:to_i).select{|i|i>0}) == self.#{assoc_key}_associations.map(&:papermill_asset_id) 73 | self.#{assoc_key}_associations.delete_all 74 | self.#{assoc_key}_associations = assets_ids.map_with_index do |asset_id, index| 75 | PapermillAssociation.new(:papermill_asset_id => asset_id, :position => (index+1)) 76 | end 77 | end 78 | end 79 | } 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /public/jgrowl/jquery.jgrowl.css: -------------------------------------------------------------------------------- 1 | div.jGrowl { 2 | padding: 10px; 3 | z-index: 9999; 4 | color: #fff; 5 | font-size: 12px; 6 | } 7 | 8 | /** Special IE6 Style Positioning **/ 9 | div.ie6 { 10 | position: absolute; 11 | } 12 | 13 | div.ie6.top-right { 14 | right: auto; 15 | bottom: auto; 16 | left: expression( ( 0 - jGrowl.offsetWidth + ( document.documentElement.clientWidth ? document.documentElement.clientWidth : document.body.clientWidth ) + ( ignoreMe2 = document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft ) ) + 'px' ); 17 | top: expression( ( 0 + ( ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop ) ) + 'px' ); 18 | } 19 | 20 | div.ie6.top-left { 21 | left: expression( ( 0 + ( ignoreMe2 = document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft ) ) + 'px' ); 22 | top: expression( ( 0 + ( ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop ) ) + 'px' ); 23 | } 24 | 25 | div.ie6.bottom-right { 26 | left: expression( ( 0 - jGrowl.offsetWidth + ( document.documentElement.clientWidth ? document.documentElement.clientWidth : document.body.clientWidth ) + ( ignoreMe2 = document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft ) ) + 'px' ); 27 | top: expression( ( 0 - jGrowl.offsetHeight + ( document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight ) + ( ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop ) ) + 'px' ); 28 | } 29 | 30 | div.ie6.bottom-left { 31 | left: expression( ( 0 + ( ignoreMe2 = document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft ) ) + 'px' ); 32 | top: expression( ( 0 - jGrowl.offsetHeight + ( document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight ) + ( ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop ) ) + 'px' ); 33 | } 34 | 35 | div.ie6.center { 36 | left: expression( ( 0 + ( ignoreMe2 = document.documentElement.scrollLeft ? document.documentElement.scrollLeft : document.body.scrollLeft ) ) + 'px' ); 37 | top: expression( ( 0 + ( ignoreMe = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop ) ) + 'px' ); 38 | width: 100%; 39 | } 40 | 41 | /** Normal Style Positions **/ 42 | div.jGrowl { 43 | position: absolute; 44 | } 45 | 46 | body > div.jGrowl { 47 | position: fixed; 48 | } 49 | 50 | div.jGrowl.top-left { 51 | left: 0px; 52 | top: 0px; 53 | } 54 | 55 | div.jGrowl.top-right { 56 | right: 0px; 57 | top: 0px; 58 | } 59 | 60 | div.jGrowl.bottom-left { 61 | left: 0px; 62 | bottom: 0px; 63 | } 64 | 65 | div.jGrowl.bottom-right { 66 | right: 0px; 67 | bottom: 0px; 68 | } 69 | 70 | div.jGrowl.center { 71 | top: 0px; 72 | width: 50%; 73 | left: 25%; 74 | } 75 | 76 | /** Cross Browser Styling **/ 77 | div.center div.jGrowl-notification, div.center div.jGrowl-closer { 78 | margin-left: auto; 79 | margin-right: auto; 80 | } 81 | 82 | div.jGrowl div.jGrowl-notification, div.jGrowl div.jGrowl-closer { 83 | background-color: #000; 84 | opacity: .85; 85 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=85)"; 86 | filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=85); 87 | zoom: 1; 88 | width: 235px; 89 | padding: 10px; 90 | margin-top: 5px; 91 | margin-bottom: 5px; 92 | font-family: Tahoma, Arial, Helvetica, sans-serif; 93 | font-size: 1em; 94 | text-align: left; 95 | display: none; 96 | -moz-border-radius: 5px; 97 | -webkit-border-radius: 5px; 98 | } 99 | 100 | div.jGrowl div.jGrowl-notification { 101 | min-height: 40px; 102 | } 103 | 104 | div.jGrowl div.jGrowl-notification div.jGrowl-header { 105 | font-weight: bold; 106 | font-size: .85em; 107 | } 108 | 109 | div.jGrowl div.jGrowl-notification div.jGrowl-close { 110 | z-index: 99; 111 | float: right; 112 | font-weight: bold; 113 | font-size: 1em; 114 | cursor: pointer; 115 | } 116 | 117 | div.jGrowl div.jGrowl-closer { 118 | padding-top: 4px; 119 | padding-bottom: 4px; 120 | cursor: pointer; 121 | font-size: .9em; 122 | font-weight: bold; 123 | text-align: center; 124 | } 125 | 126 | /** Hide jGrowl when printing **/ 127 | @media print { 128 | div.jGrowl { 129 | display: none; 130 | } 131 | } -------------------------------------------------------------------------------- /public/jgrowl/jquery.jgrowl_minimized.js: -------------------------------------------------------------------------------- 1 | (function($){$.jGrowl=function(m,o){if($('#jGrowl').size()==0) 2 | $('
').addClass($.jGrowl.defaults.position).appendTo('body');$('#jGrowl').jGrowl(m,o);};$.fn.jGrowl=function(m,o){if($.isFunction(this.each)){var args=arguments;return this.each(function(){var self=this;if($(this).data('jGrowl.instance')==undefined){$(this).data('jGrowl.instance',$.extend(new $.fn.jGrowl(),{notifications:[],element:null,interval:null}));$(this).data('jGrowl.instance').startup(this);} 3 | if($.isFunction($(this).data('jGrowl.instance')[m])){$(this).data('jGrowl.instance')[m].apply($(this).data('jGrowl.instance'),$.makeArray(args).slice(1));}else{$(this).data('jGrowl.instance').create(m,o);}});};};$.extend($.fn.jGrowl.prototype,{defaults:{pool:0,header:'',group:'',sticky:false,position:'top-right',glue:'after',theme:'default',corners:'10px',check:250,life:3000,speed:'normal',easing:'swing',closer:true,closeTemplate:'×',closerTemplate:'
[ close all ]
',log:function(e,m,o){},beforeOpen:function(e,m,o){},open:function(e,m,o){},beforeClose:function(e,m,o){},close:function(e,m,o){},animateOpen:{opacity:'show'},animateClose:{opacity:'hide'}},notifications:[],element:null,interval:null,create:function(message,o){var o=$.extend({},this.defaults,o);this.notifications.push({message:message,options:o});o.log.apply(this.element,[this.element,message,o]);},render:function(notification){var self=this;var message=notification.message;var o=notification.options;var notification=$('
'+'
'+o.closeTemplate+'
'+'
'+o.header+'
'+'
'+message+'
').data("jGrowl",o).addClass(o.theme).children('div.close').bind("click.jGrowl",function(){$(this).parent().trigger('jGrowl.close');}).parent();$(notification).bind("mouseover.jGrowl",function(){$('div.jGrowl-notification',self.element).data("jGrowl.pause",true);}).bind("mouseout.jGrowl",function(){$('div.jGrowl-notification',self.element).data("jGrowl.pause",false);}).bind('jGrowl.beforeOpen',function(){if(o.beforeOpen.apply(notification,[notification,message,o,self.element])!=false){$(this).trigger('jGrowl.open');}}).bind('jGrowl.open',function(){if(o.open.apply(notification,[notification,message,o,self.element])!=false){if(o.glue=='after'){$('div.jGrowl-notification:last',self.element).after(notification);}else{$('div.jGrowl-notification:first',self.element).before(notification);} 5 | $(this).animate(o.animateOpen,o.speed,o.easing,function(){if($.browser.msie&&(parseInt($(this).css('opacity'),10)===1||parseInt($(this).css('opacity'),10)===0)) 6 | this.style.removeAttribute('filter');$(this).data("jGrowl").created=new Date();});}}).bind('jGrowl.beforeClose',function(){if(o.beforeClose.apply(notification,[notification,message,o,self.element])!=false) 7 | $(this).trigger('jGrowl.close');}).bind('jGrowl.close',function(){$(this).data('jGrowl.pause',true);$(this).animate(o.animateClose,o.speed,o.easing,function(){$(this).remove();var close=o.close.apply(notification,[notification,message,o,self.element]);if($.isFunction(close)) 8 | close.apply(notification,[notification,message,o,self.element]);});}).trigger('jGrowl.beforeOpen');if($.fn.corner!=undefined)$(notification).corner(o.corners);if($('div.jGrowl-notification:parent',self.element).size()>1&&$('div.jGrowl-closer',self.element).size()==0&&this.defaults.closer!=false){$(this.defaults.closerTemplate).addClass('jGrowl-closer ui-state-highlight ui-corner-all').addClass(this.defaults.theme).appendTo(self.element).animate(this.defaults.animateOpen,this.defaults.speed,this.defaults.easing).bind("click.jGrowl",function(){$(this).siblings().children('div.close').trigger("click.jGrowl");if($.isFunction(self.defaults.closer)){self.defaults.closer.apply($(this).parent()[0],[$(this).parent()[0]]);}});};},update:function(){$(this.element).find('div.jGrowl-notification:parent').each(function(){if($(this).data("jGrowl")!=undefined&&$(this).data("jGrowl").created!=undefined&&($(this).data("jGrowl").created.getTime()+$(this).data("jGrowl").life)<(new Date()).getTime()&&$(this).data("jGrowl").sticky!=true&&($(this).data("jGrowl.pause")==undefined||$(this).data("jGrowl.pause")!=true)){$(this).trigger('jGrowl.beforeClose');}});if(this.notifications.length>0&&(this.defaults.pool==0||$(this.element).find('div.jGrowl-notification:parent').size()');this.interval=setInterval(function(){$(e).data('jGrowl.instance').update();},this.defaults.check);if($.browser.msie&&parseInt($.browser.version)<7&&!window["XMLHttpRequest"]){$(this.element).addClass('ie6');}},shutdown:function(){$(this.element).removeClass('jGrowl').find('div.jGrowl-notification').remove();clearInterval(this.interval);},close:function(){$(this.element).find('div.jGrowl-notification').each(function(){$(this).trigger('jGrowl.beforeClose');});}});$.jGrowl.defaults=$.fn.jGrowl.prototype.defaults;})(jQuery); 10 | -------------------------------------------------------------------------------- /public/papermill/papermill.js: -------------------------------------------------------------------------------- 1 | /* 2 | If you have your own popup solution, override popup and close_popup in your application.js 3 | 4 | * e.g. facebox : 5 | */ 6 | popup = function(url) { 7 | jQuery.facebox(function() { 8 | jQuery.get(url, function(data) { 9 | jQuery.facebox(data); 10 | }) 11 | }) 12 | return false; 13 | }; 14 | 15 | close_popup = function(source) { 16 | jQuery(document).trigger('close.facebox'); 17 | }; 18 | 19 | jQuery(document).ajaxError(function(){ 20 | switch (arguments[1].status) { 21 | case 500: 22 | notify(arguments[1].status + ": " + arguments[1].statusText, "Something bad happened on the server side.
An email has been sent, we will have a look at it shortly", "error"); break; 23 | case 404: 24 | notify(arguments[1].status + ": " + arguments[1].statusText, "Resource not found on the server.
Try to refresh the page and check if the resource still exists", "warning"); break; 25 | case 401: 26 | notify(arguments[1].status + ": " + arguments[1].statusText, "You are not allowed to access/modify this resource. Contact the website administrator", "warning"); break; 27 | case 0: 28 | notify(arguments[1].status + ": " + arguments[1].statusText, "Cannot access distant server.
Are you connected?", "warning"); break; 29 | default: 30 | notify(arguments[1].status + ": " + arguments[1].statusText, "Something happened, code=[" + arguments[1].status + "], please see this reference for further details", "error"); 31 | } 32 | }); 33 | 34 | /* 35 | If you have your own notification solution, override notify 36 | 37 | * e.g. jGrowl : 38 | */ 39 | 40 | notify = function(title, message, type) { 41 | if(type == "notice") { jQuery.jGrowl(message, { header: title, life: 4000, theme: type }) }; 42 | if(type == "warning") { jQuery.jGrowl(message, { header: title, life: 15000, theme: type }) }; 43 | if(type == "error") { jQuery.jGrowl(message, { header: title, sticky: true, theme: type }) }; 44 | }; 45 | 46 | var Papermill = { 47 | files_queued: 0, 48 | file_dialog_complete: function(num_selected, num_queued) 49 | { 50 | // SwfUpload doesn't order uploads out of the box... 51 | this.sorted_queue = []; 52 | this.index = 0; 53 | if (num_queued > 0) { 54 | file_queue = []; 55 | global_index = 0; 56 | index = 0; 57 | do { 58 | file = this.callFlash("GetFileByIndex", [global_index]); 59 | if(file != null && file.filestatus == -1) { 60 | file_queue[index] = file; 61 | index++; 62 | } 63 | global_index++; 64 | } while (file != null); 65 | this.sorted_queue = file_queue.sort(function(a,b){ 66 | if(b.name < a.name) { return (1) } else { return (-1) } 67 | }); 68 | var self = this; 69 | jQuery(this.sorted_queue).each( function(index, file) { 70 | div = jQuery('
').attr({ 'id': file.id, 'class': 'swfupload asset' }); 71 | div.append(jQuery('').attr('class', 'name').html(file.name.substring(0, 10) + '...')); 72 | div.append(jQuery('').attr('class', 'progress').append('')); 73 | if(self.settings.file_queue_limit == 1) { 74 | old_asset = jQuery("#" + self.settings.upload_id).children()[0]; 75 | if(old_asset) { 76 | jQuery(old_asset).hide(); 77 | } 78 | } 79 | jQuery("#" + self.settings.upload_id).append(div); 80 | }); 81 | this.startUpload(this.sorted_queue[this.index++].id); 82 | } 83 | }, 84 | upload_start: function(file) 85 | { 86 | this.addFileParam(file.id, "Fileid", file.id); 87 | if(this.settings.file_queue_limit == 1) { 88 | old_asset = jQuery("#" + this.settings.upload_id).children()[0]; 89 | if (old_asset && old_asset.id != file.id) { 90 | this.addFileParam(file.id, "Oldfileid", old_asset.id); 91 | } 92 | } 93 | }, 94 | upload_progress: function(file, bytes, total) 95 | { 96 | percent = Math.ceil((bytes / total) * 100); 97 | jQuery('#' + file.id + ' .progress span').width(percent + '%'); 98 | }, 99 | upload_error: function(file, errorCode, message) { 100 | switch (errorCode) { 101 | case SWFUpload.UPLOAD_ERROR.UPLOAD_LIMIT_EXCEEDED: 102 | notify("Error", "Too many files selected", "error"); break; 103 | default: 104 | notify("Error", "An error occurred while sending " + file.name, "error"); 105 | } 106 | jQuery("#"+file.id).remove(); 107 | }, 108 | upload_success: function(file, data) 109 | { 110 | eval(data); 111 | }, 112 | upload_complete: function(file) 113 | { 114 | Papermill.files_queued -= 1; 115 | if(this.sorted_queue[this.index]) { 116 | this.startUpload(this.sorted_queue[this.index++].id); 117 | } 118 | }, 119 | file_queue_error: function(file, errorCode, message) { 120 | switch (errorCode) { 121 | case SWFUpload.QUEUE_ERROR.QUEUE_LIMIT_EXCEEDED: 122 | notify("Error", "Too many files selected", "error"); break; 123 | case SWFUpload.QUEUE_ERROR.FILE_EXCEEDS_SIZE_LIMIT: 124 | notify("Error", file.name + " is too big", "error"); break; 125 | case SWFUpload.QUEUE_ERROR.ZERO_BYTE_FILE: 126 | notify("Error", file.name + " is empty", "error"); break; 127 | case SWFUpload.QUEUE_ERROR.INVALID_FILETYPE: 128 | notify("Error", file.name + " is not an allowed file type", "error"); break; 129 | default: 130 | notify("Error", "An error occurred while sending " + file.name, "error"); 131 | } 132 | jQuery("#"+file.id).remove(); 133 | }, 134 | modify_all: function(papermill_id) { 135 | container = jQuery("#" + papermill_id)[0]; 136 | attribute_name = jQuery("#batch_" + papermill_id)[0].value; 137 | attribute_wording = jQuery("#batch_" + papermill_id + " option:selected").text(); 138 | value = prompt(attribute_wording + ":"); 139 | if(value != null) { 140 | jQuery.ajax({async:true, data:jQuery(container).sortable('serialize') + "&list_id=" + container.id, dataType:'script', type:'post', url:'/papermill/mass_edit?attribute=' + attribute_name + "&value=" + value}); 141 | } 142 | } 143 | }; 144 | 145 | -------------------------------------------------------------------------------- /lib/papermill/papermill_asset.rb: -------------------------------------------------------------------------------- 1 | class PapermillAsset < ActiveRecord::Base 2 | before_destroy :destroy_files 3 | 4 | has_attached_file :file, 5 | :processors => [:papermill_paperclip_processor], 6 | :url => "#{Papermill::options[:papermill_url_prefix]}/#{Papermill::compute_paperclip_path.gsub(':style', ':escape_style_in_url')}", 7 | :path => "#{Papermill::options[:papermill_path_prefix]}/#{Papermill::compute_paperclip_path.gsub(':style', ':escape_style_in_path')}" 8 | 9 | before_post_process :set_file_name 10 | 11 | validates_attachment_presence :file 12 | 13 | belongs_to :assetable, :polymorphic => true 14 | has_many :papermill_associations, :dependent => :delete_all 15 | 16 | named_scope :papermill, lambda { |assetable_type, assetable_id, assetable_key, through| 17 | through ? 18 | { :joins => "INNER JOIN papermill_associations ON papermill_assets.id = papermill_associations.papermill_asset_id \ 19 | AND papermill_associations.assetable_type = #{connection.quote assetable_type} \ 20 | AND papermill_associations.assetable_id = #{assetable_id.to_i} \ 21 | AND papermill_associations.assetable_key = #{connection.quote assetable_key}", 22 | :order => "papermill_associations.position" } : 23 | { :conditions => { 24 | :assetable_type => assetable_type, 25 | :assetable_id => assetable_id, 26 | :assetable_key => assetable_key.to_s }, 27 | :order => "papermill_assets.position" } 28 | } 29 | 30 | def assetable_type=(sType) 31 | super(sType.to_s.classify.constantize.base_class.to_s) 32 | end 33 | 34 | Paperclip.interpolates :url_key do |attachment, style| 35 | attachment.instance.compute_url_key((style || "original").to_s) 36 | end 37 | 38 | Paperclip.interpolates :escape_style_in_url do |attachment, style| 39 | # double escaping needed for windows (complains about '< > " | / \' ), to match escaped filesystem from front webserver pov 40 | s = (style || "original").to_s 41 | Papermill::MSWIN ? CGI::escape(CGI::escape(s)) : CGI::escape(s) 42 | end 43 | 44 | Paperclip.interpolates :escape_style_in_path do |attachment, style| 45 | s = (style || "original").to_s 46 | Papermill::MSWIN ? CGI::escape(s) : s 47 | end 48 | 49 | attr_accessor :crop_h, :crop_w, :crop_x, :crop_y 50 | 51 | def Filedata=(data) 52 | if !Papermill::MSWIN && !(mime = `file --mime -br #{data.path}`).blank? && !mime.starts_with?("cannot open") 53 | data.content_type = mime.strip.split(";").first 54 | elsif (mime = MIME::Types.type_for(data.original_filename)) 55 | data.content_type = mime.first.simplified 56 | end 57 | self.file = data 58 | end 59 | 60 | def Filename=(name) 61 | @real_file_name = name 62 | end 63 | 64 | def id_partition 65 | ("%09d" % self.id).scan(/\d{3}/).join("/") 66 | end 67 | 68 | def name 69 | file_file_name 70 | end 71 | 72 | def basename 73 | name.gsub(/#{extension}$/, "").strip 74 | end 75 | 76 | def extension 77 | File.extname(name) 78 | end 79 | 80 | def width 81 | @width ||= Paperclip::Geometry.from_file(file).width 82 | end 83 | 84 | def height 85 | @height ||= Paperclip::Geometry.from_file(file).height 86 | end 87 | 88 | def size 89 | file_file_size 90 | end 91 | 92 | def url(style = nil) 93 | return url!(style) if style.is_a?(Hash) 94 | file.url(style) 95 | end 96 | 97 | def path(style = nil) 98 | return path!(style) if style.is_a?(Hash) 99 | file.path(style) 100 | end 101 | 102 | def url!(style = nil) 103 | create_thumb_file(style_name = style_name(style), style) unless File.exists?(self.path(style_name)) 104 | file.url(style_name) 105 | end 106 | 107 | def path!(style = nil) 108 | create_thumb_file(style_name = style_name(style), style) unless File.exists?(self.path(style_name)) 109 | file.path(style_name) 110 | end 111 | 112 | def content_type 113 | file_content_type 114 | end 115 | 116 | def style_name(style) 117 | style.is_a?(Hash) ? (style[:name] || style.hash).to_s : (style || "original").to_s 118 | end 119 | 120 | def self.papermill_options(assetable_type, assetable_key) 121 | if assetable_type 122 | assoc = assetable_type.constantize.papermill_options 123 | assoc[assetable_key.try(:to_sym)] || assoc[:default] || Papermill::options 124 | else 125 | Papermill::options 126 | end 127 | end 128 | 129 | def papermill_options 130 | self.class.papermill_options(assetable_type, assetable_key) 131 | end 132 | 133 | def image? 134 | content_type.split("/")[0] == "image" 135 | end 136 | 137 | def create_thumb_file(style_name, style = nil) 138 | return false unless self.image? 139 | destroy_thumbnails if style_name.to_s == "original" 140 | style = self.class.compute_style(style_name) unless style.is_a?(Hash) 141 | FileUtils.mkdir_p File.dirname(new_path = file.path(style_name)) 142 | FileUtils.cp((tmp_path = Paperclip::PapermillPaperclipProcessor.make(file, style).path), new_path) 143 | FileUtils.chmod(0644, new_path) unless Papermill::MSWIN 144 | File.delete(tmp_path) 145 | return true 146 | end 147 | 148 | def destroy_thumbnails 149 | thumbnail_folder_mask = Papermill::options[:use_url_key] ? "*/*/" : "*/" 150 | original_folder = "#{File.dirname(file.path)}/" 151 | Dir.glob("#{root_directory}/#{thumbnail_folder_mask}").each do |f| 152 | FileUtils.rm_r(f) unless f == original_folder 153 | end 154 | Dir.glob("#{root_directory}/*/").each do |f| 155 | FileUtils.rm_r(f) if Dir.entries(f) == [".", ".."] 156 | end 157 | end 158 | 159 | def self.destroy_orphans 160 | self.name != self.base_class.name ? 161 | PapermillAsset.delete_all(["created_at < ? AND assetable_id IS NULL AND type = ?", 1.hour.ago, self.name]) : 162 | PapermillAsset.delete_all(["created_at < ? AND assetable_id IS NULL AND type IS NULL", 1.hour.ago]) 163 | end 164 | 165 | def compute_url_key(style) 166 | Papermill::options[:url_key_generator].call(style, self) 167 | end 168 | 169 | def has_valid_url_key?(key, style) 170 | !Papermill::options[:use_url_key] || compute_url_key(style) == key 171 | end 172 | 173 | private 174 | 175 | def root_directory 176 | deepness_to_root = Papermill::options[:use_url_key] ? -3 : -2 177 | @root_directory ||= File.dirname(path).split('/')[0..deepness_to_root].join('/') 178 | end 179 | 180 | def set_file_name 181 | return if @real_file_name.blank? 182 | self.title = (basename = @real_file_name.gsub(/#{extension = File.extname(@real_file_name)}$/, "")) 183 | self.file.instance_write(:file_name, "#{basename.to_url}#{extension}") 184 | end 185 | 186 | def destroy_files 187 | FileUtils.rm_r(root_directory) rescue true 188 | end 189 | 190 | def self.compute_style(style) 191 | style = Papermill::options[:aliases][style.to_sym] || Papermill::options[:aliases][style.to_s] || !Papermill::options[:alias_only] && style 192 | [Symbol, String].include?(style.class) ? { :geometry => style.to_s } : style 193 | end 194 | end -------------------------------------------------------------------------------- /lib/papermill/form_builder.rb: -------------------------------------------------------------------------------- 1 | class ActionView::Helpers::FormBuilder 2 | include ActionView::Helpers::FormTagHelper 3 | 4 | def assets_upload(method, options = {}) 5 | papermill_upload_tag method, { :thumbnail => false }.update(options) 6 | end 7 | def asset_upload(method, options = {}) 8 | papermill_upload_tag method, { :gallery => false, :thumbnail => false }.update(options) 9 | end 10 | def images_upload(method, options = {}) 11 | papermill_upload_tag method, options 12 | end 13 | def image_upload(method, options = {}) 14 | papermill_upload_tag method, { :gallery => false }.update(options) 15 | end 16 | end 17 | 18 | module ActionView::Helpers::FormTagHelper 19 | 20 | def assets_upload_tag(assetable, method, options = {}) 21 | papermill_upload_tag method, { :thumbnail => false, :assetable => assetable }.update(options) 22 | end 23 | 24 | def asset_upload_tag(assetable, method, options = {}) 25 | papermill_upload_tag method, { :gallery => false, :thumbnail => false, :assetable => assetable }.update(options) 26 | end 27 | 28 | def images_upload_tag(assetable, method, options = {}) 29 | papermill_upload_tag method, { :assetable => assetable }.update(options) 30 | end 31 | 32 | def image_upload_tag(assetable, method, options = {}) 33 | papermill_upload_tag method, { :gallery => false, :assetable => assetable }.update(options) 34 | end 35 | 36 | private 37 | 38 | def papermill_upload_tag(method, options) 39 | assetable = options[:object] || options[:assetable] || @object || @template.instance_variable_get("@#{@object_name}") 40 | 41 | raise PapermillException.new("Form object not found. Please provide it with :object => @assetable with the Papermill helper call") unless assetable 42 | assetable_name = @object_name && "#{@object_name}#{(i = @options[:index]) ? "[#{i}]" : ""}" 43 | 44 | sanitized_method = method.to_s.gsub(/[\?\/\-]$/, '') 45 | sanitized_object_name = @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") 46 | field_id = @object_name && "#{sanitized_object_name}#{(i = @options[:index]) ? "_#{i}" : ""}_#{sanitized_method}" 47 | assetable.class.papermill(method) 48 | association_options = assetable.class.papermill_options[method.to_sym] 49 | 50 | raise PapermillException.new("Papermill association #{method} failed to be generated dynamically for #{assetable.class.name}") unless association_options 51 | options = association_options.deep_merge(options) 52 | field_name = "#{assetable_name}[papermill_#{method}_ids][]" 53 | 54 | html = {} 55 | 56 | if ot = options[:thumbnail] 57 | w = ot[:width] || ot[:height] && ot[:aspect_ratio] && (ot[:height] * ot[:aspect_ratio]).to_i || nil 58 | h = ot[:height] || ot[:width] && ot[:aspect_ratio] && (ot[:width] / ot[:aspect_ratio]).to_i || nil 59 | computed_style = ot[:style] || (w || h) && "#{w}x#{h}>" || "original" 60 | html[:css] = set_papermill_inline_css(field_id, w, h, options) 61 | end 62 | 63 | html[:js] = set_papermill_inline_js(field_id, compute_papermill_url(:create, computed_style, field_name, field_id, options), options) 64 | 65 | 66 | html[:upload_button] = %{\ 67 |
68 | 69 |
} 70 | 71 | # I don't use the full :through association that is not updated if assetable.new_record?. 72 | collection = association_options[:through] ? assetable.send("#{method}_associations").map(&:papermill_asset) : assetable.send(method) 73 | locals = { :thumbnail_style => computed_style, :targetted_size => options[:targetted_size], :field_name => field_name, :field_id => field_id } 74 | html[:container] = @template.content_tag(:div, :id => field_id, :class => "papermill-#{method.to_s} #{(options[:thumbnail] ? "papermill-thumb-container" : "papermill-asset-container")} #{(options[:gallery] ? "papermill-multiple-items" : "papermill-unique-item")}") do 75 | @template.render(:partial => "papermill/asset", 76 | :collection => collection, 77 | :locals => locals) 78 | end 79 | 80 | if options[:gallery] && options[:mass_edit] 81 | html[:mass_edit] = %{\ 82 | #{I18n.t("papermill.modify-all")} 83 | } 86 | end 87 | 88 | if options[:through] 89 | browser_url = compute_papermill_url(:browser, computed_style, field_name, field_id, options) 90 | html[:browser] = %{Ajouter...} 91 | end 92 | 93 | # hidden_field needed to empty a list. 94 | %{
95 | #{@template.hidden_field("#{assetable_name}[papermill_#{method}_ids]", nil)} 96 | #{html[:css].to_s + html[:js].to_s + options[:form_helper_elements].map{|element| html[element] || ""}.join("\n")} 97 |
}.html_safe 98 | end 99 | 100 | 101 | def compute_papermill_url(action, computed_style, field_name, field_id, options) 102 | @template.url_for({ 103 | :escape => false, :controller => "/papermill", :action => action, 104 | :asset_class => (options[:class_name] || PapermillAsset).to_s, 105 | :gallery => !!options[:gallery], :thumbnail_style => computed_style, :targetted_size => options[:targetted_size], 106 | :field_name => field_name, :field_id => field_id 107 | }) 108 | end 109 | 110 | def set_papermill_inline_js(field_id, create_url, options) 111 | outputed_js = (options[:gallery] ? %{ jQuery("##{field_id}").sortable() } : "") + 112 | %{ 113 | new SWFUpload({ 114 | post_params: { 115 | "#{ ActionController::Base.session_options[:key] }": "#{ @template.cookies[ActionController::Base.session_options[:key]] }" 116 | }, 117 | upload_id: "#{ field_id }", 118 | upload_url: "#{ @template.escape_javascript create_url }", 119 | file_types: "#{ options[:swfupload].delete(:file_types) || (options[:images_only] ? '*.jpg;*.jpeg;*.png;*.gif' : '*.*') }", 120 | file_queue_limit: "#{ !options[:gallery] ? '1' : '0' }", 121 | file_queued_handler: Papermill.file_queued, 122 | file_dialog_complete_handler: Papermill.file_dialog_complete, 123 | upload_start_handler: Papermill.upload_start, 124 | upload_progress_handler: Papermill.upload_progress, 125 | file_queue_error_handler: Papermill.file_queue_error, 126 | upload_error_handler: Papermill.upload_error, 127 | upload_success_handler: Papermill.upload_success, 128 | upload_complete_handler: Papermill.upload_complete, 129 | button_placeholder_id: "browse_for_#{field_id}", 130 | #{ options[:swfupload].map { |key, value| "#{key}: #{value}" if value }.compact.join(",\n") } 131 | }); 132 | } 133 | if options[:use_content_for] 134 | @template.content_for :papermill_inline_js do 135 | outputed_js 136 | end 137 | "" 138 | else 139 | %{} 144 | end 145 | end 146 | 147 | def set_papermill_inline_css(field_id, width, height, options) 148 | return unless options[:inline_css] 149 | html = ["\n"] 150 | size = [width && "width:#{width}px", height && "height:#{height}px"].compact.join("; ") 151 | if og = options[:gallery] 152 | vp, hp, vm, hm, b = [og[:vpadding], og[:hpadding], og[:vmargin], og[:hmargin], og[:border_thickness]].map &:to_i 153 | gallery_width = (og[:width] || width) && "width:#{og[:width] || og[:columns]*(width.to_i+(hp+hm+b)*2)}px;" || "" 154 | gallery_height = (og[:height] || height) && "min-height:#{og[:height] || og[:lines]*(height.to_i+(vp+vm+b)*2)}px;" || "" 155 | html << %{##{field_id} { #{gallery_width} #{gallery_height} }} 156 | html << %{##{field_id} .asset { margin:#{vm}px #{hm}px; border-width:#{b}px; padding:#{vp}px #{hp}px; #{size}; }} 157 | else 158 | html << %{##{field_id}, ##{field_id} .asset { #{size} }} 159 | end 160 | html << %{##{field_id} .name { width:#{width || "100"}px; }} 161 | if options[:use_content_for] 162 | @template.content_for :papermill_inline_css do 163 | html.join("\n") 164 | end 165 | "" 166 | else 167 | %{} 170 | end 171 | end 172 | end -------------------------------------------------------------------------------- /public/facebox/facebox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Facebox (for jQuery) 3 | * version: 1.2 (05/05/2008) 4 | * @requires jQuery v1.2 or later 5 | * 6 | * Examples at http://famspam.com/facebox/ 7 | * 8 | * Licensed under the MIT: 9 | * http://www.opensource.org/licenses/mit-license.php 10 | * 11 | * Copyright 2007, 2008 Chris Wanstrath [ chris@ozmm.org ] 12 | * 13 | * Usage: 14 | * 15 | * jQuery(document).ready(function() { 16 | * jQuery('a[rel*=facebox]').facebox() 17 | * }) 18 | * 19 | * Terms 20 | * Loads the #terms div in the box 21 | * 22 | * Terms 23 | * Loads the terms.html page in the box 24 | * 25 | * Terms 26 | * Loads the terms.png image in the box 27 | * 28 | * 29 | * You can also use it programmatically: 30 | * 31 | * jQuery.facebox('some html') 32 | * 33 | * The above will open a facebox with "some html" as the content. 34 | * 35 | * jQuery.facebox(function($) { 36 | * $.get('blah.html', function(data) { $.facebox(data) }) 37 | * }) 38 | * 39 | * The above will show a loading screen before the passed function is called, 40 | * allowing for a better ajaxy experience. 41 | * 42 | * The facebox function can also display an ajax page or image: 43 | * 44 | * jQuery.facebox({ ajax: 'remote.html' }) 45 | * jQuery.facebox({ image: 'dude.jpg' }) 46 | * 47 | * Want to close the facebox? Trigger the 'close.facebox' document event: 48 | * 49 | * jQuery(document).trigger('close.facebox') 50 | * 51 | * Facebox also has a bunch of other hooks: 52 | * 53 | * loading.facebox 54 | * beforeReveal.facebox 55 | * reveal.facebox (aliased as 'afterReveal.facebox') 56 | * init.facebox 57 | * 58 | * Simply bind a function to any of these hooks: 59 | * 60 | * $(document).bind('reveal.facebox', function() { ...stuff to do after the facebox and contents are revealed... }) 61 | * 62 | */ 63 | (function($) { 64 | $.facebox = function(data, klass) { 65 | $.facebox.loading() 66 | 67 | if (data.ajax) fillFaceboxFromAjax(data.ajax) 68 | else if (data.image) fillFaceboxFromImage(data.image) 69 | else if (data.div) fillFaceboxFromHref(data.div) 70 | else if ($.isFunction(data)) data.call($) 71 | else $.facebox.reveal(data, klass) 72 | } 73 | 74 | /* 75 | * Public, $.facebox methods 76 | */ 77 | 78 | $.extend($.facebox, { 79 | settings: { 80 | opacity : 0, 81 | overlay : true, 82 | loadingImage : '/facebox/loading.gif', 83 | closeImage : '/facebox/closelabel.gif', 84 | imageTypes : [ 'png', 'jpg', 'jpeg', 'gif' ], 85 | faceboxHtml : '\ 86 | ' 113 | }, 114 | 115 | loading: function() { 116 | init() 117 | if ($('#facebox .loading').length == 1) return true 118 | showOverlay() 119 | 120 | $('#facebox .content').empty() 121 | $('#facebox .body').children().hide().end(). 122 | append('
') 123 | 124 | $('#facebox').css({ 125 | top: getPageScroll()[1] + (getPageHeight() / 10), 126 | left: 385.5 127 | }).show() 128 | 129 | $(document).bind('keydown.facebox', function(e) { 130 | if (e.keyCode == 27) $.facebox.close() 131 | return true 132 | }) 133 | $(document).trigger('loading.facebox') 134 | }, 135 | 136 | reveal: function(data, klass) { 137 | $(document).trigger('beforeReveal.facebox') 138 | if (klass) $('#facebox .content').addClass(klass) 139 | $('#facebox .content').append(data) 140 | $('#facebox .loading').remove() 141 | $('#facebox .body').children().fadeIn('normal') 142 | $('#facebox').css('left', $(window).width() / 2 - ($('#facebox table').width() / 2)) 143 | $(document).trigger('reveal.facebox').trigger('afterReveal.facebox') 144 | }, 145 | 146 | close: function() { 147 | $(document).trigger('close.facebox') 148 | return false 149 | } 150 | }) 151 | 152 | /* 153 | * Public, $.fn methods 154 | */ 155 | 156 | $.fn.facebox = function(settings) { 157 | init(settings) 158 | 159 | function clickHandler() { 160 | $.facebox.loading(true) 161 | 162 | // support for rel="facebox.inline_popup" syntax, to add a class 163 | // also supports deprecated "facebox[.inline_popup]" syntax 164 | var klass = this.rel.match(/facebox\[?\.(\w+)\]?/) 165 | if (klass) klass = klass[1] 166 | 167 | fillFaceboxFromHref(this.href, klass) 168 | return false 169 | } 170 | 171 | return this.click(clickHandler) 172 | } 173 | 174 | /* 175 | * Private methods 176 | */ 177 | 178 | // called one time to setup facebox on this page 179 | function init(settings) { 180 | if ($.facebox.settings.inited) return true 181 | else $.facebox.settings.inited = true 182 | 183 | $(document).trigger('init.facebox') 184 | makeCompatible() 185 | 186 | var imageTypes = $.facebox.settings.imageTypes.join('|') 187 | $.facebox.settings.imageTypesRegexp = new RegExp('\.' + imageTypes + '$', 'i') 188 | 189 | if (settings) $.extend($.facebox.settings, settings) 190 | $('body').append($.facebox.settings.faceboxHtml) 191 | 192 | var preload = [ new Image(), new Image() ] 193 | preload[0].src = $.facebox.settings.closeImage 194 | preload[1].src = $.facebox.settings.loadingImage 195 | 196 | $('#facebox').find('.b:first, .bl, .br, .tl, .tr').each(function() { 197 | preload.push(new Image()) 198 | preload.slice(-1).src = $(this).css('background-image').replace(/url\((.+)\)/, '$1') 199 | }) 200 | 201 | $('#facebox .close').click($.facebox.close) 202 | $('#facebox .close_image').attr('src', $.facebox.settings.closeImage) 203 | } 204 | 205 | // getPageScroll() by quirksmode.com 206 | function getPageScroll() { 207 | var xScroll, yScroll; 208 | if (self.pageYOffset) { 209 | yScroll = self.pageYOffset; 210 | xScroll = self.pageXOffset; 211 | } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict 212 | yScroll = document.documentElement.scrollTop; 213 | xScroll = document.documentElement.scrollLeft; 214 | } else if (document.body) {// all other Explorers 215 | yScroll = document.body.scrollTop; 216 | xScroll = document.body.scrollLeft; 217 | } 218 | return new Array(xScroll,yScroll) 219 | } 220 | 221 | // Adapted from getPageSize() by quirksmode.com 222 | function getPageHeight() { 223 | var windowHeight 224 | if (self.innerHeight) { // all except Explorer 225 | windowHeight = self.innerHeight; 226 | } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode 227 | windowHeight = document.documentElement.clientHeight; 228 | } else if (document.body) { // other Explorers 229 | windowHeight = document.body.clientHeight; 230 | } 231 | return windowHeight 232 | } 233 | 234 | // Backwards compatibility 235 | function makeCompatible() { 236 | var $s = $.facebox.settings 237 | 238 | $s.loadingImage = $s.loading_image || $s.loadingImage 239 | $s.closeImage = $s.close_image || $s.closeImage 240 | $s.imageTypes = $s.image_types || $s.imageTypes 241 | $s.faceboxHtml = $s.facebox_html || $s.faceboxHtml 242 | } 243 | 244 | // Figures out what you want to display and displays it 245 | // formats are: 246 | // div: #id 247 | // image: blah.extension 248 | // ajax: anything else 249 | function fillFaceboxFromHref(href, klass) { 250 | // div 251 | if (href.match(/#/)) { 252 | var url = window.location.href.split('#')[0] 253 | var target = href.replace(url,'') 254 | $.facebox.reveal($(target).clone().show(), klass) 255 | 256 | // image 257 | } else if (href.match($.facebox.settings.imageTypesRegexp)) { 258 | fillFaceboxFromImage(href, klass) 259 | // ajax 260 | } else { 261 | fillFaceboxFromAjax(href, klass) 262 | } 263 | } 264 | 265 | function fillFaceboxFromImage(href, klass) { 266 | var image = new Image() 267 | image.onload = function() { 268 | $.facebox.reveal('
', klass) 269 | } 270 | image.src = href 271 | } 272 | 273 | function fillFaceboxFromAjax(href, klass) { 274 | $.get(href, function(data) { $.facebox.reveal(data, klass) }) 275 | } 276 | 277 | function skipOverlay() { 278 | return $.facebox.settings.overlay == false || $.facebox.settings.opacity === null 279 | } 280 | 281 | function showOverlay() { 282 | if (skipOverlay()) return 283 | 284 | if ($('facebox_overlay').length == 0) 285 | $("body").append('
') 286 | 287 | $('#facebox_overlay').hide().addClass("facebox_overlayBG") 288 | .css('opacity', $.facebox.settings.opacity) 289 | .click(function() { $(document).trigger('close.facebox') }) 290 | .fadeIn(200) 291 | return false 292 | } 293 | 294 | function hideOverlay() { 295 | if (skipOverlay()) return 296 | 297 | $('#facebox_overlay').fadeOut(200, function(){ 298 | $("#facebox_overlay").removeClass("facebox_overlayBG") 299 | $("#facebox_overlay").addClass("facebox_hide") 300 | $("#facebox_overlay").remove() 301 | }) 302 | 303 | return false 304 | } 305 | 306 | /* 307 | * Bindings 308 | */ 309 | 310 | $(document).bind('close.facebox', function() { 311 | $(document).unbind('keydown.facebox') 312 | $('#facebox').fadeOut(function() { 313 | $('#facebox .content').removeClass().addClass('content') 314 | hideOverlay() 315 | $('#facebox .loading').remove() 316 | }) 317 | }) 318 | 319 | })(jQuery); 320 | -------------------------------------------------------------------------------- /generators/papermill_initializer/papermill_initializer.rb: -------------------------------------------------------------------------------- 1 | # DO NOT MOVE OR RENAME THIS FILE 2 | # It must stand in your RAILS_ROOT/config/initializer folder and be named papermill.rb, because it is explicitely early-loaded by Papermill 3 | 4 | module Papermill 5 | 6 | # All options here already are defaults. 7 | 8 | unless defined?(OPTIONS) 9 | 10 | OPTIONS = { 11 | 12 | 13 | #@@@@@@@@@@@@@@ Papermill association parameters @@@@@@@@@@@@@@@@@@ 14 | 15 | # You can override these parameters here, or in your papermill associations definition. 16 | 17 | # Associated PapermillAsset subclass (must be an STI subclass of PapermillAsset) 18 | # :class_name => "PapermillAsset", 19 | 20 | # You can use the included join table if you need your assets to be associated to more than one assetable 21 | # :through => false, 22 | 23 | 24 | #@@@@@@@@@@@@@@@@@@@ form-helper parameters @@@@@@@@@@@@@@@@@@@@@@@ 25 | 26 | # You can override all these parameters here, or in your papermill associations definition, or in form-helper calls. 27 | 28 | # Helper can generates inline css styling that adapt to your gallery/images placeholder. You can use it to scaffold, then copy the lines you need in your application css and set it to false. 29 | 30 | # :inline_css => true, 31 | 32 | # If you don't want generated CSS and JS to be captured and available through @papermill_inline_css/@papermill_inline_js, and instead directly outputed in the form, set it to false 33 | 34 | # :use_content_for => true, 35 | 36 | # SwfUpload will only let the user upload images. 37 | 38 | # :images_only => false, 39 | 40 | # Dashboard is only for galleries 41 | # You can remove/change order of HTML elements. 42 | 43 | # :form_helper_elements => [:upload_button, :container, :mass_edit], 44 | 45 | # Batch edit a single field of all the assets in a form field 46 | 47 | # :mass_edit => true, 48 | 49 | # Attributes editable at once for all assets in a gallery 50 | 51 | # :mass_editable_fields => ["title", "copyright", "description"], 52 | 53 | # Attributes you can edit in the form. You can use :type (string or text) and :label (any string) 54 | # if you have more complex needs, you should override app/views/papermill/_form.html.erb in your application. 55 | 56 | # :editable_fields => [ 57 | # {:title => {:type => "string"}}, 58 | # {:alt => {:type => "string"}}, 59 | # {:copyright => {:type => "string"}}, 60 | # {:description => {:type => "text" }}, 61 | # ], 62 | 63 | # FormHelper gallery options 64 | # If :inline_css is true, css will be generated automatically and added through @content_for_papermill_inline_css (papermill_stylesheet_tag includes it) 65 | # Great for quick admin scaffolding. 66 | 67 | :gallery => { 68 | 69 | # override calculated gallery width. Ex: "auto" 70 | 71 | # :width => nil, 72 | 73 | # override calculated gallery height 74 | 75 | # :height => nil, 76 | 77 | # Number of columns and lines in a gallery 78 | 79 | # :columns => 8, 80 | # :lines => 2, 81 | 82 | # vertical/horizontal padding/margin around each thumbnails 83 | 84 | # :vpadding => 0, 85 | # :hpadding => 0, 86 | # :vmargin => 1, 87 | # :hmargin => 1, 88 | 89 | # border around thumbnails 90 | 91 | # :border_thickness => 2 92 | }, 93 | 94 | # FormHelper thumbnail's information. 95 | # Set :width OR :height to nil to use aspect_ratio value. Remember that 4/3 == 1 => Use : 4.0/3 96 | # You can override computed ImageMagick transformation strings that defaults to "#{:width}x#{:height}>" by setting a value to :style 97 | # Needed if you set :aliases_only to true 98 | 99 | :thumbnail => { 100 | 101 | # :width => 100, 102 | # :height => 100, 103 | # :aspect_ratio => nil, 104 | # :style => nil 105 | }, 106 | 107 | # If you plan to use the original images in one place in one specific size, you can pass targetted_size, 108 | # to incitate the user to crop the image himself (double-click on the image when editing) with the right parameters, 109 | # instead of letting ImageMagick do his magick bluntly at render-time. 110 | 111 | # :targetted_size => nil, 112 | 113 | # Options passed on to SWFUpload. 114 | # To remove an option when overriding, set it to nil. 115 | 116 | :swfupload => { 117 | # :flash_url => "'/swfupload/swfupload.swf'", 118 | # :button_image_url => "'/papermill/images/upload-blank.png'", 119 | # :button_width => 61, 120 | # :button_height => 22, 121 | # :button_text => %{'#{I18n.t("papermill.upload-button-wording")}'}, 122 | # :button_text_style => %{'.button-text { font-size: 12pt; font-weight: bold; }'}, 123 | # :button_text_top_padding => 4, 124 | # :button_text_left_padding => 4, 125 | # :debug => false, 126 | # :prevent_swf_caching => true, 127 | # :file_size_limit => "'10 MB'" 128 | }, 129 | 130 | #@@@@@@@@@@@@@@@@@ thumbnails style parameters @@@@@@@@@@@@@@@@@@@@ 131 | 132 | # You can override all these parameters here, or in your papermill associations definition, or in thumbnail styling hashes. 133 | 134 | # 1. COPYRIGHT WATERMARKING 135 | 136 | # Activate with '©' at the end of your geometry string or pass :copyright => true|"my_copyright" in alias definition or geometry hash 137 | # Papermill will use, in that order of priority : 138 | # * copyright found in geometry string AFTER the @ 139 | # * alternatively :copyright in alias/definition hash 140 | # * asset's copyright column (if found) 141 | # * associated :copyright definition in your Assetable association definition 142 | # * below's :copyright definition 143 | 144 | # Set this definition to a default copyright name if you want one (You'll still need to postfix your geometry string with © or pass {:watermark => true} in your alias/geometry hash to use it) 145 | 146 | # :copyright => nil, 147 | 148 | # Textilize, truncate, transform... your copyright before its ImageMagick integration 149 | 150 | # :copyright_text_transform => Proc.new {|c| c }, 151 | 152 | # Watermark ImageMagick command string. 153 | # * %s gets interpolated with above transformed copyright string 154 | # * DO NOT change the background color!, change the bordercolor instead. (because background color adds to bordercolor I set it to totally transparent) 155 | # * for both fill (=foreground color) and bordercolor (=background color), the last two octals control alpha (transparency). FF is opaque, 00 is transparent. 156 | # * remove -bordercolor if you don't want any background 157 | # * add +antialias to REMOVE antialiasing (after '-font Arial-Bold', for example) 158 | # * font-size is pointsize 159 | # * type 'identify -list font' to get a list of the fonts you can use (ImageMagick will default to Arial/Times if it can't find it) 160 | # * use -gravity and -geometry for positionning, -geometry +x+y is relative to -gravity's corner/zone 161 | # * parenthesis are there to isolate the label creation from the compositing (blending) part. 162 | # * don't touch things you don't understand, you'll save yourself some time 163 | 164 | # :copyright_im_command => %{ \\( -font Arial-Bold -pointsize 9 -fill '#FFFFFFE0' -border 3 -bordercolor '#50550080' -background '#00000000' label:' %s ' \\) -gravity South-West -geometry +0+0 -composite }, 165 | 166 | # 2. IMAGE WATERMARKING 167 | 168 | # Activate with "-wm" at the end of your geometry string BEFORE copyright ('©'), or alternatively with :watermark => in your alias/inline hash 169 | # If you pass an image_path to :watermark, it will override below : 170 | 171 | # you can use a relative path from your public directory (see :public_root), a complete path, or an URI 172 | 173 | # :watermark => "/images/rails.png", 174 | 175 | # default :watermarking command for image_magick. %s gets interpolated with above image path. 176 | 177 | # :watermark_im_command => %{- | composite \\( %s -resize 100% \\) - -dissolve 20% -gravity center -geometry +0+0 }, 178 | 179 | 180 | 181 | #@@@@@@@@@@@@@@@@@ Application-wide parameters @@@@@@@@@@@@@@@@@@@@ 182 | 183 | # Set to true to require aliases in all url/path, disabling the 184 | # Don't forget to give an alias value to options[:thumbnail][:style] if true! 185 | 186 | # :alias_only => false, 187 | 188 | # Needed if :alias_only 189 | # Aliases are available application wide for all your assets. You can pass them instead of a geometry_string or a geometry hash 190 | 191 | :aliases => { 192 | # :mini_crop => "100x100#", 193 | # :cant_touch_this => { 194 | # :geometry => "400x>", 195 | # :copyright => "You sire, can't touch this", 196 | # :watermark => "http://westsidewill.com/newblog/wp-content/uploads/2009/09/MC-Hammer.jpg" 197 | # } 198 | }, 199 | 200 | # To prevent generation of thumbnails and guessing of assets location through URL hacking 201 | # e.g. if you want to protect access to non-copyrighted original files, 202 | # or don't want users to browse images by guessing the sequence of ids, 203 | # an encrypted hash can be generated for each geometry string/alias and added to path/url. 204 | # Use your imagination for :url_key_generator, really. 205 | # Please note that all previous assets paths will be lost if you add/remove or change the :url_key generation. 206 | 207 | # :use_url_key => false, 208 | # :url_key_salt => "change-me-to-your-favorite-pet's-name", 209 | # :url_key_generator => Proc.new { |style, asset| Digest::SHA512.hexdigest("#{style}#{asset.id}#{Papermill::options[:url_key_salt]}")[0..10] }, 210 | 211 | # added before path/url. Your front webserver will need to be able to find your assets 212 | # :papermill_path_prefix => ":rails_root/public/system/papermill", 213 | # :papermill_url_prefix => "/system/papermill", 214 | 215 | # you can set it to false if you don't plan to have too many assets. (dangerous) 216 | # :use_id_partition => true, 217 | 218 | # If you use those defaults, the first asset will end-up in RAILS_ROOT/public/system/papermill/000/000/001/original/my_first_asset_name.ext 219 | # You'll access it with my_domain.com/system/papermill/000/000/001/original/my_first_asset_name.ext 220 | } 221 | end 222 | end 223 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Papermill 2 | 3 | * Asset management made easy, 10 minutes integration. 4 | * All-you-can-eat glue around Polymorphic Paperclip table, SWFUpload & JQuery. 5 | * Associate any image or list of images with any model and any key. 6 | 7 | == Install the gem 8 | 9 | sudo gem install papermill 10 | 11 | == Try the demo 12 | 13 | rails -m http://github.com/bbenezech/papermill/raw/master/demo.txt papermill-example 14 | 15 | === Out-of-the-box compatibility with : 16 | 17 | * Formtastic # use :as => :[image|asset](s)_upload 18 | * JGrowl # for notifications (included) 19 | * FaceBox # for popups (included) 20 | * Stringex # (or any String#to_url) for asset filename/url generation 21 | 22 | === Navigator minimal requirements: 23 | 24 | * IE6+ 25 | * Flash 9+ 26 | * Javascript ON 27 | 28 | Check your audience. 29 | 30 | === Server requirements: 31 | 32 | * Rails 2.3.[4~>8] 33 | * Paperclip 2.3.1.1 (loaded with gem dependency) 34 | * Front web server serving static assets if present, and forwarding demand to rails if not. Any classic installation will do that by default. 35 | * NOT compatible with Heroku/S3 36 | 37 | == Installation 38 | 39 | === Once gem is installed 40 | 41 | Generate the migration 42 | ./script/generate papermill_table PapermillMigration 43 | 44 | Edit it and migrate 45 | rake db:migrate 46 | 47 | Copy static assets to your public directory 48 | ./script/generate papermill_assets 49 | 50 | Create the option file config/initializers/papermill.rb 51 | ./script/generate papermill_initializer 52 | 53 | Go have a look at config/initializers/papermill.rb 54 | 55 | === In environment.rb 56 | 57 | ... 58 | Rails::Initializer.run do |config| 59 | ... 60 | config.gem papermill 61 | end 62 | 63 | === In your layout 64 | 65 | 66 | ==== Quick version 67 | 68 | Inside 69 | <%= papermill_stylesheet_tag %> 70 | 71 | Before (best practice for javascript loading) 72 | <%= papermill_javascript_tag :with_jquery => "no_conflict" %> 73 | 74 | You don't need :with_jquery if load it by yourself. Pass "no_conflict" if you use the default Prototype library, or some other '$' library (mootools..) 75 | 76 | ==== In a real-world production application, you could use something like this, and adapt it to your own needs 77 | 78 | Inside 79 | <% unless @content_for_papermill_inline_js.blank? %> 80 | <%= javascript_include_tag "/facebox/facebox.js", "/jgrowl/jquery.jgrowl_minimized.js", "/papermill/jquery.Jcrop.min.js", "/swfupload/swfupload.js", "/papermill/papermill.js", :cache => "papermill" %> 81 | 86 | <% end %> 87 | 88 | Before 89 | 90 | 91 | <% unless @content_for_papermill_inline_js.blank? %> 92 | <%= stylesheet_link_tag("/facebox/facebox.css", "/jgrowl/jquery.jgrowl.css", "/Jcrop/jquery.Jcrop.css", "/papermill/papermill.css", :cache => "papermill") %> 93 | 96 | <% end %> 97 | 98 | == Security 99 | 100 | === URL-hacking 101 | 102 | Maybe you don't want users to use your application as a thumbnailing farm for their own uploaded images, or you have protected members areas and you don't want users to 'browse' others members file. 103 | 104 | * Brute solution: pass :use_url_key to true in the options (config/initializers/papermill.rb). A crypted hash unique to your application and to each asset and to the requested style will be added to the URL. No more happy-guessing of anything. Do that first before going live, or you'll have to migrate all assets... 105 | * pass :alias_only to true. This will disable the possibility to generate thumbnails with a papermill string in the url, but won't do anything for the member area thing. Plus you will have to use aliases only, form helpers included (pass :thumbnail => { :style => :some_alias }) 106 | 107 | == Usage 108 | 109 | Assetable is the class that has_many papermill_assets (i.e. the class with the papermill declaration) 110 | 111 | === Assetable declaration 112 | 113 | You can have one :default association (his settings will be used for unfound associations) and as many other associations as you want in your model. 114 | You can define a papermill relationship dynamically: just do smtg like Assetable.papermill(:dynamic_key, {}) when you need to. Perfect for CMS where associations are created by users. Then you'll be able to use assetable.dynamic_key to retrieve the associated assets. If you don't send the {}, default options from default association will be used, which may or may not be what you want. 115 | 116 | Actually, the form helper leverages this when you use a :key that doesn't exist: it will create a new Papermill relationship whith :key as the name and options from the :default declaration if any found on the model. 117 | 118 | If you don't need dynamic keys, just declare your associations in the model, like this : 119 | 120 | class Article 121 | papermill :default 122 | papermill :images 123 | papermill :pdf_version 124 | papermill :cover_image 125 | papermill :illustrations 126 | end 127 | 128 | === Form helpers 129 | 130 | Example form: 131 | 132 | form_for @assetable do 133 | # I need a simple asset upload field : 134 | f.asset_upload :pdf_version 135 | 136 | # Now I need to be able to upload as many documents as I need, and sort them at will 137 | # no document should be bigger than 1MB (respect the quoting!) 138 | # and I don't want the mass_edit feature 139 | f.assets_upload :documentation, :swfupload => { :file_size_limit => "'1 MB'" }, :mass_edit => false 140 | 141 | # I need to display *one* cover *image*, format will be 200x200 142 | # targetted_size will give the uploader hints when cropping the image after upload : desired display size and wanted aspect-ratio. 143 | # Better than cropping automatically in the center if the character's head is in the upper-left corner.. 144 | # :thumbnail => { :width & :height } set the dimensions of the preview thumbnail 145 | # And finally, I need a 200x200# crop for preview, not the default 200x200> that would be generated by default ("#{:width}x#{:heigth}>") 146 | f.image_upload :cover_image, :targetted_size => "200x200", :thumbnail => { :width => 200, :height => 200, :style => "200x200#" } 147 | 148 | # Now the image gallery, sortable. 149 | # I use :gallery => { :lines & :columns } to give the number of lines/columns, 150 | # and some CSS will be generated to size the gallery perfectly, 151 | # according to the thumb size inside the gallery and their padding/margin/border sizes. 152 | # the number of lines will increase if needed when uploading 153 | f.images_upload :illustrations, { 154 | :thumbnail => { 155 | :width => 100, 156 | :height => 70 157 | }, 158 | :gallery => { 159 | :columns => 8, # number of columns 160 | :lines => 2, # number of lines 161 | :vpadding => 2, # vertical padding around each thumb 162 | :hpadding => 2, # horizontal one 163 | :vmargin => 3, # vertical margin 164 | :hmargin => 1, # horizontal one 165 | :border_thickness => 2 # border size around each thumb 166 | } 167 | } 168 | end 169 | 170 | With Formtastic, pass 171 | 172 | :as => (:image_upload | :images_upload | :asset_upload | :assets_upload) 173 | And add your options as you would with the normal helpers. 174 | 175 | With FormTagHelpers, use (image_upload_tag | images_upload_tag | asset_upload_tag | assets_upload_tag) @assetable, :key, options 176 | 177 | image_upload_tag @article, :cover_image, :targetted_size => "200x200" 178 | 179 | === Asset editing 180 | 181 | * double-click on any uploaded asset in any form-helper to access & edit his properties 182 | * then double-click image to crop it if it's an image. You'll then access a Jcrop window. Pass :targetted_size => "widthxheigth" to lock aspect-ratio and default the selection size to widthxheigth. 183 | 184 | === Thumbnails 185 | 186 | 187 | ==== On-the-fly request time processing: 188 | 189 | PapermillAsset#url(papermill string (see 1.)) # path and url behave the same way 190 | PapermillAsset#url(papermill alias (see 2.)) 191 | 192 | Pros: fast. Nothing done upon page rendering. If asset isn't found by Apache/NGinx, then request is passed to rails, which will create it, once. 193 | 194 | Cons: need to setup an alias in the options if you want to define use a hash instead of a papermill string (for custom watermark) 195 | 196 | ==== Render time processing: 197 | 198 | PapermillAsset#url!(papermill string (see 1.)) # path! and url! behave the same way 199 | PapermillAsset#url!(papermill alias (see 2.)) 200 | PapermillAsset#url!(papermill hash (see 3.)) 201 | 202 | Pros: can use a hash directly in the url call. 203 | 204 | Cons: needs a thumbnail presence check at each render. 205 | 206 | ==== 1. Papermill String 207 | 208 | Consist of: 209 | 210 | * an ImageMagick geometry string (ex: "100x100>", "original", "100x#", etc.) 211 | * an optional watermark (-wm) flag # will use option[:watemark] for URI 212 | * an optional copyright (©) flag # will use copyright text after the "©" or options[:copyright] 213 | 214 | Examples: 215 | 216 | image_tag @article.covers.first.url("100x100") 217 | image_tag @article.covers.first.url("original©") 218 | image_tag @article.covers.first.url("100x100#-wm©") 219 | image_tag @article.covers.first.url("100x200#©papermill") 220 | 221 | ==== 2. Papermill Alias 222 | 223 | Those are application-wide, set them in the options 224 | 225 | Consist of: 226 | 227 | :geometry => "ImageMagick-geometry-string" 228 | :copyright => true | "copyright" # If true, the asset copyright field will be used. Edit the asset. 229 | :watermark => true | URI # If true, will use options[:watemark] 230 | 231 | Examples: 232 | 233 | #config/initilializers/papermill.rb 234 | 235 | # snip 236 | :aliases => { 237 | :thumb_copyrighted => { 238 | :geometry => "100x100", 239 | :copyright => "papermill", 240 | }, 241 | :thumb_copyrighted_dynamically => { 242 | :geometry => "100x100", 243 | :copyright => true 244 | }, 245 | :thumb_watermarked_with_rails => { 246 | :width => "100", 247 | :height => "100", 248 | :watermark => "/images/rails.png" 249 | } 250 | } 251 | 252 | Then in your views, simply do 253 | 254 | image_tag @article.covers.first.url(:thumb_copyrighted) 255 | 256 | ==== 3. Papermill Hash 257 | 258 | Same as aliases, but defined directly in #url!() 259 | Plus you can add a :name that will be used for style-name (defaults to a md5 of the hash) 260 | 261 | Example: 262 | 263 | image_tag @article.covers.first.url( 264 | :geometry => "100x100", 265 | :watermark => "/images/rails.png", 266 | :copyright => "papermill", 267 | :name => "thumbnail_watermarked_and_copyrighted" 268 | ) 269 | 270 | === Resource access 271 | 272 | Papermill generates an # association 273 | 274 | @entry.mug_shots.first 275 | @entry.diaporamas.each do |image| .. 276 | # etc. 277 | 278 | === Using PapermillAsset 279 | 280 | @asset = @entry.mug_shots.first 281 | image_tag @asset.url # original 282 | image_tag @asset.url("100x>") # assuming asset is an image 283 | image_tag @asset.url(:big) # assuming you have a :big alias 284 | @asset.name 285 | @asset.content_type 286 | @asset.path 287 | @asset.path("100x>") 288 | # etc. 289 | 290 | === Translations: 291 | 292 | Papermill is fully I18n-able. 293 | Copy config/locales/papermill.yml to your root config/locale folder to modify any wording in a any locale. 294 | 295 | Copyright (c) 2009 Benoit Bénézech, released under the MIT license 296 | 297 | http://rubyonrails.org/images/rails.png -------------------------------------------------------------------------------- /public/jgrowl/jquery.jgrowl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jGrowl 1.2.5 3 | * 4 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 5 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 6 | * 7 | * Written by Stan Lemon 8 | * Last updated: 2009.12.15 9 | * 10 | * jGrowl is a jQuery plugin implementing unobtrusive userland notifications. These 11 | * notifications function similarly to the Growl Framework available for 12 | * Mac OS X (http://growl.info). 13 | * 14 | * To Do: 15 | * - Move library settings to containers and allow them to be changed per container 16 | * 17 | * Changes in 1.2.5 18 | * - Changed wrapper jGrowl's options usage to "o" instead of $.jGrowl.defaults 19 | * - Added themeState option to control 'highlight' or 'error' for jQuery UI 20 | * - Ammended some CSS to provide default positioning for nested usage. 21 | * - Changed some CSS to be prefixed with jGrowl- to prevent namespacing issues 22 | * 23 | * Changes in 1.2.4 24 | * - Fixed IE bug with the close-all button 25 | * - Fixed IE bug with the filter CSS attribute (special thanks to gotwic) 26 | * - Update IE opacity CSS 27 | * - Changed font sizes to use "em", and only set the base style 28 | * 29 | * Changes in 1.2.3 30 | * - The callbacks no longer use the container as context, instead they use the actual notification 31 | * - The callbacks now receive the container as a parameter after the options parameter 32 | * - beforeOpen and beforeClose now check the return value, if it's false - the notification does 33 | * not continue. The open callback will also halt execution if it returns false. 34 | * - Fixed bug where containers would get confused 35 | * - Expanded the pause functionality to pause an entire container. 36 | * 37 | * Changes in 1.2.2 38 | * - Notification can now be theme rolled for jQuery UI, special thanks to Jeff Chan! 39 | * 40 | * Changes in 1.2.1 41 | * - Fixed instance where the interval would fire the close method multiple times. 42 | * - Added CSS to hide from print media 43 | * - Fixed issue with closer button when div { position: relative } is set 44 | * - Fixed leaking issue with multiple containers. Special thanks to Matthew Hanlon! 45 | * 46 | * Changes in 1.2.0 47 | * - Added message pooling to limit the number of messages appearing at a given time. 48 | * - Closing a notification is now bound to the notification object and triggered by the close button. 49 | * 50 | * Changes in 1.1.2 51 | * - Added iPhone styled example 52 | * - Fixed possible IE7 bug when determining if the ie6 class shoudl be applied. 53 | * - Added template for the close button, so that it's content could be customized. 54 | * 55 | * Changes in 1.1.1 56 | * - Fixed CSS styling bug for ie6 caused by a mispelling 57 | * - Changes height restriction on default notifications to min-height 58 | * - Added skinned examples using a variety of images 59 | * - Added the ability to customize the content of the [close all] box 60 | * - Added jTweet, an example of using jGrowl + Twitter 61 | * 62 | * Changes in 1.1.0 63 | * - Multiple container and instances. 64 | * - Standard $.jGrowl() now wraps $.fn.jGrowl() by first establishing a generic jGrowl container. 65 | * - Instance methods of a jGrowl container can be called by $.fn.jGrowl(methodName) 66 | * - Added glue preferenced, which allows notifications to be inserted before or after nodes in the container 67 | * - Added new log callback which is called before anything is done for the notification 68 | * - Corner's attribute are now applied on an individual notification basis. 69 | * 70 | * Changes in 1.0.4 71 | * - Various CSS fixes so that jGrowl renders correctly in IE6. 72 | * 73 | * Changes in 1.0.3 74 | * - Fixed bug with options persisting across notifications 75 | * - Fixed theme application bug 76 | * - Simplified some selectors and manipulations. 77 | * - Added beforeOpen and beforeClose callbacks 78 | * - Reorganized some lines of code to be more readable 79 | * - Removed unnecessary this.defaults context 80 | * - If corners plugin is present, it's now customizable. 81 | * - Customizable open animation. 82 | * - Customizable close animation. 83 | * - Customizable animation easing. 84 | * - Added customizable positioning (top-left, top-right, bottom-left, bottom-right, center) 85 | * 86 | * Changes in 1.0.2 87 | * - All CSS styling is now external. 88 | * - Added a theme parameter which specifies a secondary class for styling, such 89 | * that notifications can be customized in appearance on a per message basis. 90 | * - Notification life span is now customizable on a per message basis. 91 | * - Added the ability to disable the global closer, enabled by default. 92 | * - Added callbacks for when a notification is opened or closed. 93 | * - Added callback for the global closer. 94 | * - Customizable animation speed. 95 | * - jGrowl now set itself up and tears itself down. 96 | * 97 | * Changes in 1.0.1: 98 | * - Removed dependency on metadata plugin in favor of .data() 99 | * - Namespaced all events 100 | */ 101 | (function($) { 102 | 103 | /** jGrowl Wrapper - Establish a base jGrowl Container for compatibility with older releases. **/ 104 | $.jGrowl = function( m , o ) { 105 | // To maintain compatibility with older version that only supported one instance we'll create the base container. 106 | if ( $('#jGrowl').size() == 0 ) 107 | $('
').addClass( (o && o.position) ? o.position : $.jGrowl.defaults.position ).appendTo('body'); 108 | 109 | // Create a notification on the container. 110 | $('#jGrowl').jGrowl(m,o); 111 | }; 112 | 113 | 114 | /** Raise jGrowl Notification on a jGrowl Container **/ 115 | $.fn.jGrowl = function( m , o ) { 116 | if ( $.isFunction(this.each) ) { 117 | var args = arguments; 118 | 119 | return this.each(function() { 120 | var self = this; 121 | 122 | /** Create a jGrowl Instance on the Container if it does not exist **/ 123 | if ( $(this).data('jGrowl.instance') == undefined ) { 124 | $(this).data('jGrowl.instance', $.extend( new $.fn.jGrowl(), { notifications: [], element: null, interval: null } )); 125 | $(this).data('jGrowl.instance').startup( this ); 126 | } 127 | 128 | /** Optionally call jGrowl instance methods, or just raise a normal notification **/ 129 | if ( $.isFunction($(this).data('jGrowl.instance')[m]) ) { 130 | $(this).data('jGrowl.instance')[m].apply( $(this).data('jGrowl.instance') , $.makeArray(args).slice(1) ); 131 | } else { 132 | $(this).data('jGrowl.instance').create( m , o ); 133 | } 134 | }); 135 | }; 136 | }; 137 | 138 | $.extend( $.fn.jGrowl.prototype , { 139 | 140 | /** Default JGrowl Settings **/ 141 | defaults: { 142 | pool: 0, 143 | header: '', 144 | group: '', 145 | sticky: false, 146 | position: 'top-right', 147 | glue: 'after', 148 | theme: 'default', 149 | themeState: 'highlight', 150 | corners: '10px', 151 | check: 250, 152 | life: 3000, 153 | speed: 'normal', 154 | easing: 'swing', 155 | closer: true, 156 | closeTemplate: '×', 157 | closerTemplate: '
[ close all ]
', 158 | log: function(e,m,o) {}, 159 | beforeOpen: function(e,m,o) {}, 160 | open: function(e,m,o) {}, 161 | beforeClose: function(e,m,o) {}, 162 | close: function(e,m,o) {}, 163 | animateOpen: { 164 | opacity: 'show' 165 | }, 166 | animateClose: { 167 | opacity: 'hide' 168 | } 169 | }, 170 | 171 | notifications: [], 172 | 173 | /** jGrowl Container Node **/ 174 | element: null, 175 | 176 | /** Interval Function **/ 177 | interval: null, 178 | 179 | /** Create a Notification **/ 180 | create: function( message , o ) { 181 | var o = $.extend({}, this.defaults, o); 182 | 183 | this.notifications.push({ message: message , options: o }); 184 | 185 | o.log.apply( this.element , [this.element,message,o] ); 186 | }, 187 | 188 | render: function( notification ) { 189 | var self = this; 190 | var message = notification.message; 191 | var o = notification.options; 192 | 193 | var notification = $( 194 | '
' + 196 | '
' + o.closeTemplate + '
' + 197 | '
' + o.header + '
' + 198 | '
' + message + '
' 199 | ).data("jGrowl", o).addClass(o.theme).children('div.jGrowl-close').bind("click.jGrowl", function() { 200 | $(this).parent().trigger('jGrowl.close'); 201 | }).parent(); 202 | 203 | 204 | /** Notification Actions **/ 205 | $(notification).bind("mouseover.jGrowl", function() { 206 | $('div.jGrowl-notification', self.element).data("jGrowl.pause", true); 207 | }).bind("mouseout.jGrowl", function() { 208 | $('div.jGrowl-notification', self.element).data("jGrowl.pause", false); 209 | }).bind('jGrowl.beforeOpen', function() { 210 | if ( o.beforeOpen.apply( notification , [notification,message,o,self.element] ) != false ) { 211 | $(this).trigger('jGrowl.open'); 212 | } 213 | }).bind('jGrowl.open', function() { 214 | if ( o.open.apply( notification , [notification,message,o,self.element] ) != false ) { 215 | if ( o.glue == 'after' ) { 216 | $('div.jGrowl-notification:last', self.element).after(notification); 217 | } else { 218 | $('div.jGrowl-notification:first', self.element).before(notification); 219 | } 220 | 221 | $(this).animate(o.animateOpen, o.speed, o.easing, function() { 222 | // Fixes some anti-aliasing issues with IE filters. 223 | if ($.browser.msie && (parseInt($(this).css('opacity'), 10) === 1 || parseInt($(this).css('opacity'), 10) === 0)) 224 | this.style.removeAttribute('filter'); 225 | 226 | $(this).data("jGrowl").created = new Date(); 227 | }); 228 | } 229 | }).bind('jGrowl.beforeClose', function() { 230 | if ( o.beforeClose.apply( notification , [notification,message,o,self.element] ) != false ) 231 | $(this).trigger('jGrowl.close'); 232 | }).bind('jGrowl.close', function() { 233 | // Pause the notification, lest during the course of animation another close event gets called. 234 | $(this).data('jGrowl.pause', true); 235 | $(this).animate(o.animateClose, o.speed, o.easing, function() { 236 | $(this).remove(); 237 | var close = o.close.apply( notification , [notification,message,o,self.element] ); 238 | 239 | if ( $.isFunction(close) ) 240 | close.apply( notification , [notification,message,o,self.element] ); 241 | }); 242 | }).trigger('jGrowl.beforeOpen'); 243 | 244 | /** Optional Corners Plugin **/ 245 | if ( $.fn.corner != undefined ) $(notification).corner( o.corners ); 246 | 247 | /** Add a Global Closer if more than one notification exists **/ 248 | if ( $('div.jGrowl-notification:parent', self.element).size() > 1 && 249 | $('div.jGrowl-closer', self.element).size() == 0 && this.defaults.closer != false ) { 250 | $(this.defaults.closerTemplate).addClass('jGrowl-closer ui-state-highlight ui-corner-all').addClass(this.defaults.theme) 251 | .appendTo(self.element).animate(this.defaults.animateOpen, this.defaults.speed, this.defaults.easing) 252 | .bind("click.jGrowl", function() { 253 | $(this).siblings().children('div.close').trigger("click.jGrowl"); 254 | 255 | if ( $.isFunction( self.defaults.closer ) ) { 256 | self.defaults.closer.apply( $(this).parent()[0] , [$(this).parent()[0]] ); 257 | } 258 | }); 259 | }; 260 | }, 261 | 262 | /** Update the jGrowl Container, removing old jGrowl notifications **/ 263 | update: function() { 264 | $(this.element).find('div.jGrowl-notification:parent').each( function() { 265 | if ( $(this).data("jGrowl") != undefined && $(this).data("jGrowl").created != undefined && 266 | ($(this).data("jGrowl").created.getTime() + $(this).data("jGrowl").life) < (new Date()).getTime() && 267 | $(this).data("jGrowl").sticky != true && 268 | ($(this).data("jGrowl.pause") == undefined || $(this).data("jGrowl.pause") != true) ) { 269 | 270 | // Pause the notification, lest during the course of animation another close event gets called. 271 | $(this).trigger('jGrowl.beforeClose'); 272 | } 273 | }); 274 | 275 | if ( this.notifications.length > 0 && 276 | (this.defaults.pool == 0 || $(this.element).find('div.jGrowl-notification:parent').size() < this.defaults.pool) ) 277 | this.render( this.notifications.shift() ); 278 | 279 | if ( $(this.element).find('div.jGrowl-notification:parent').size() < 2 ) { 280 | $(this.element).find('div.jGrowl-closer').animate(this.defaults.animateClose, this.defaults.speed, this.defaults.easing, function() { 281 | $(this).remove(); 282 | }); 283 | } 284 | }, 285 | 286 | /** Setup the jGrowl Notification Container **/ 287 | startup: function(e) { 288 | this.element = $(e).addClass('jGrowl').append('
'); 289 | this.interval = setInterval( function() { 290 | $(e).data('jGrowl.instance').update(); 291 | }, this.defaults.check); 292 | 293 | if ($.browser.msie && parseInt($.browser.version) < 7 && !window["XMLHttpRequest"]) { 294 | $(this.element).addClass('ie6'); 295 | } 296 | }, 297 | 298 | /** Shutdown jGrowl, removing it and clearing the interval **/ 299 | shutdown: function() { 300 | $(this.element).removeClass('jGrowl').find('div.jGrowl-notification').remove(); 301 | clearInterval( this.interval ); 302 | }, 303 | 304 | close: function() { 305 | $(this.element).find('div.jGrowl-notification').each(function(){ 306 | $(this).trigger('jGrowl.beforeClose'); 307 | }); 308 | } 309 | }); 310 | 311 | /** Reference the Defaults Object for compatibility with older versions of jGrowl **/ 312 | $.jGrowl.defaults = $.fn.jGrowl.prototype.defaults; 313 | 314 | })(jQuery); 315 | -------------------------------------------------------------------------------- /public/Jcrop/jquery.Jcrop.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Jcrop v.0.9.8 (minimized) 3 | * (c) 2008 Kelly Hallman and DeepLiquid.com 4 | * More information: http://deepliquid.com/content/Jcrop.html 5 | * Released under MIT License - this header must remain with code 6 | */ 7 | 8 | 9 | (function($){$.Jcrop=function(obj,opt) 10 | {var obj=obj,opt=opt;if(typeof(obj)!=='object')obj=$(obj)[0];if(typeof(opt)!=='object')opt={};if(!('trackDocument'in opt)) 11 | {opt.trackDocument=$.browser.msie?false:true;if($.browser.msie&&$.browser.version.split('.')[0]=='8') 12 | opt.trackDocument=true;} 13 | if(!('keySupport'in opt)) 14 | opt.keySupport=$.browser.msie?false:true;var defaults={trackDocument:false,baseClass:'jcrop',addClass:null,bgColor:'black',bgOpacity:.6,borderOpacity:.4,handleOpacity:.5,handlePad:5,handleSize:9,handleOffset:5,edgeMargin:14,aspectRatio:0,keySupport:true,cornerHandles:true,sideHandles:true,drawBorders:true,dragEdges:true,boxWidth:0,boxHeight:0,boundary:8,animationDelay:20,swingSpeed:3,allowSelect:true,allowMove:true,allowResize:true,minSelect:[0,0],maxSize:[0,0],minSize:[0,0],onChange:function(){},onSelect:function(){}};var options=defaults;setOptions(opt);var $origimg=$(obj);var $img=$origimg.clone().removeAttr('id').css({position:'absolute'});$img.width($origimg.width());$img.height($origimg.height());$origimg.after($img).hide();presize($img,options.boxWidth,options.boxHeight);var boundx=$img.width(),boundy=$img.height(),$div=$('
').width(boundx).height(boundy).addClass(cssClass('holder')).css({position:'relative',backgroundColor:options.bgColor}).insertAfter($origimg).append($img);;if(options.addClass)$div.addClass(options.addClass);var $img2=$('').attr('src',$img.attr('src')).css('position','absolute').width(boundx).height(boundy);var $img_holder=$('
').width(pct(100)).height(pct(100)).css({zIndex:310,position:'absolute',overflow:'hidden'}).append($img2);var $hdl_holder=$('
').width(pct(100)).height(pct(100)).css('zIndex',320);var $sel=$('
').css({position:'absolute',zIndex:300}).insertBefore($img).append($img_holder,$hdl_holder);var bound=options.boundary;var $trk=newTracker().width(boundx+(bound*2)).height(boundy+(bound*2)).css({position:'absolute',top:px(-bound),left:px(-bound),zIndex:290}).mousedown(newSelection);var xlimit,ylimit,xmin,ymin;var xscale,yscale,enabled=true;var docOffset=getPos($img),btndown,lastcurs,dimmed,animating,shift_down;var Coords=function() 15 | {var x1=0,y1=0,x2=0,y2=0,ox,oy;function setPressed(pos) 16 | {var pos=rebound(pos);x2=x1=pos[0];y2=y1=pos[1];};function setCurrent(pos) 17 | {var pos=rebound(pos);ox=pos[0]-x2;oy=pos[1]-y2;x2=pos[0];y2=pos[1];};function getOffset() 18 | {return[ox,oy];};function moveOffset(offset) 19 | {var ox=offset[0],oy=offset[1];if(0>x1+ox)ox-=ox+x1;if(0>y1+oy)oy-=oy+y1;if(boundyboundx) 28 | {xx=boundx;h=Math.abs((xx-x1)/aspect);yy=rh<0?y1-h:h+y1;}} 29 | else 30 | {xx=x2;h=rwa/aspect;yy=rh<0?y1-h:y1+h;if(yy<0) 31 | {yy=0;w=Math.abs((yy-y1)*aspect);xx=rw<0?x1-w:w+x1;} 32 | else if(yy>boundy) 33 | {yy=boundy;w=Math.abs(yy-y1)*aspect;xx=rw<0?x1-w:w+x1;}} 34 | if(xx>x1){if(xx-x1max_x){xx=x1+max_x;} 35 | if(yy>y1){yy=y1+(xx-x1)/aspect;}else{yy=y1-(xx-x1)/aspect;}}else if(xxmax_x){xx=x1-max_x;} 36 | if(yy>y1){yy=y1+(x1-xx)/aspect;}else{yy=y1-(x1-xx)/aspect;}} 37 | if(xx<0){x1-=xx;xx=0;}else if(xx>boundx){x1-=xx-boundx;xx=boundx;} 38 | if(yy<0){y1-=yy;yy=0;}else if(yy>boundy){y1-=yy-boundy;yy=boundy;} 39 | return last=makeObj(flipCoords(x1,y1,xx,yy));};function rebound(p) 40 | {if(p[0]<0)p[0]=0;if(p[1]<0)p[1]=0;if(p[0]>boundx)p[0]=boundx;if(p[1]>boundy)p[1]=boundy;return[p[0],p[1]];};function flipCoords(x1,y1,x2,y2) 41 | {var xa=x1,xb=x2,ya=y1,yb=y2;if(x2xlimit)) 47 | x2=(xsize>0)?(x1+xlimit):(x1-xlimit);if(ylimit&&(Math.abs(ysize)>ylimit)) 48 | y2=(ysize>0)?(y1+ylimit):(y1-ylimit);if(ymin&&(Math.abs(ysize)0)?(y1+ymin):(y1-ymin);if(xmin&&(Math.abs(xsize)0)?(x1+xmin):(x1-xmin);if(x1<0){x2-=x1;x1-=x1;} 51 | if(y1<0){y2-=y1;y1-=y1;} 52 | if(x2<0){x1-=x2;x2-=x2;} 53 | if(y2<0){y1-=y2;y2-=y2;} 54 | if(x2>boundx){var delta=x2-boundx;x1-=delta;x2-=delta;} 55 | if(y2>boundy){var delta=y2-boundy;y1-=delta;y2-=delta;} 56 | if(x1>boundx){var delta=x1-boundy;y2-=delta;y1-=delta;} 57 | if(y1>boundy){var delta=y1-boundy;y2-=delta;y1-=delta;} 58 | return makeObj(flipCoords(x1,y1,x2,y2));};function makeObj(a) 59 | {return{x:a[0],y:a[1],x2:a[2],y2:a[3],w:a[2]-a[0],h:a[3]-a[1]};};return{flipCoords:flipCoords,setPressed:setPressed,setCurrent:setCurrent,getOffset:getOffset,moveOffset:moveOffset,getCorner:getCorner,getFixed:getFixed};}();var Selection=function() 60 | {var start,end,dragmode,awake,hdep=370;var borders={};var handle={};var seehandles=false;var hhs=options.handleOffset;if(options.drawBorders){borders={top:insertBorder('hline').css('top',$.browser.msie?px(-1):px(0)),bottom:insertBorder('hline'),left:insertBorder('vline'),right:insertBorder('vline')};} 61 | if(options.dragEdges){handle.t=insertDragbar('n');handle.b=insertDragbar('s');handle.r=insertDragbar('e');handle.l=insertDragbar('w');} 62 | options.sideHandles&&createHandles(['n','s','e','w']);options.cornerHandles&&createHandles(['sw','nw','ne','se']);function insertBorder(type) 63 | {var jq=$('
').css({position:'absolute',opacity:options.borderOpacity}).addClass(cssClass(type));$img_holder.append(jq);return jq;};function dragDiv(ord,zi) 64 | {var jq=$('
').mousedown(createDragger(ord)).css({cursor:ord+'-resize',position:'absolute',zIndex:zi});$hdl_holder.append(jq);return jq;};function insertHandle(ord) 65 | {return dragDiv(ord,hdep++).css({top:px(-hhs+1),left:px(-hhs+1),opacity:options.handleOpacity}).addClass(cssClass('handle'));};function insertDragbar(ord) 66 | {var s=options.handleSize,o=hhs,h=s,w=s,t=o,l=o;switch(ord) 67 | {case'n':case's':w=pct(100);break;case'e':case'w':h=pct(100);break;} 68 | return dragDiv(ord,hdep++).width(w).height(h).css({top:px(-t+1),left:px(-l+1)});};function createHandles(li) 69 | {for(i in li)handle[li[i]]=insertHandle(li[i]);};function moveHandles(c) 70 | {var midvert=Math.round((c.h/2)-hhs),midhoriz=Math.round((c.w/2)-hhs),north=west=-hhs+1,east=c.w-hhs,south=c.h-hhs,x,y;'e'in handle&&handle.e.css({top:px(midvert),left:px(east)})&&handle.w.css({top:px(midvert)})&&handle.s.css({top:px(south),left:px(midhoriz)})&&handle.n.css({left:px(midhoriz)});'ne'in handle&&handle.ne.css({left:px(east)})&&handle.se.css({top:px(south),left:px(east)})&&handle.sw.css({top:px(south)});'b'in handle&&handle.b.css({top:px(south)})&&handle.r.css({left:px(east)});};function moveto(x,y) 71 | {$img2.css({top:px(-y),left:px(-x)});$sel.css({top:px(y),left:px(x)});};function resize(w,h) 72 | {$sel.width(w).height(h);};function refresh() 73 | {var c=Coords.getFixed();Coords.setPressed([c.x,c.y]);Coords.setCurrent([c.x2,c.y2]);updateVisible();};function updateVisible() 74 | {if(awake)return update();};function update() 75 | {var c=Coords.getFixed();resize(c.w,c.h);moveto(c.x,c.y);options.drawBorders&&borders['right'].css({left:px(c.w-1)})&&borders['bottom'].css({top:px(c.h-1)});seehandles&&moveHandles(c);awake||show();options.onChange(unscale(c));};function show() 76 | {$sel.show();$img.css('opacity',options.bgOpacity);awake=true;};function release() 77 | {disableHandles();$sel.hide();$img.css('opacity',1);awake=false;};function showHandles() 78 | {if(seehandles) 79 | {moveHandles(Coords.getFixed());$hdl_holder.show();}};function enableHandles() 80 | {seehandles=true;if(options.allowResize) 81 | {moveHandles(Coords.getFixed());$hdl_holder.show();return true;}};function disableHandles() 82 | {seehandles=false;$hdl_holder.hide();};function animMode(v) 83 | {(animating=v)?disableHandles():enableHandles();};function done() 84 | {animMode(false);refresh();};var $track=newTracker().mousedown(createDragger('move')).css({cursor:'move',position:'absolute',zIndex:360}) 85 | $img_holder.append($track);disableHandles();return{updateVisible:updateVisible,update:update,release:release,refresh:refresh,setCursor:function(cursor){$track.css('cursor',cursor);},enableHandles:enableHandles,enableOnly:function(){seehandles=true;},showHandles:showHandles,disableHandles:disableHandles,animMode:animMode,done:done};}();var Tracker=function() 86 | {var onMove=function(){},onDone=function(){},trackDoc=options.trackDocument;if(!trackDoc) 87 | {$trk.mousemove(trackMove).mouseup(trackUp).mouseout(trackUp);} 88 | function toFront() 89 | {$trk.css({zIndex:450});if(trackDoc) 90 | {$(document).mousemove(trackMove).mouseup(trackUp);}} 91 | function toBack() 92 | {$trk.css({zIndex:290});if(trackDoc) 93 | {$(document).unbind('mousemove',trackMove).unbind('mouseup',trackUp);}} 94 | function trackMove(e) 95 | {onMove(mouseAbs(e));};function trackUp(e) 96 | {e.preventDefault();e.stopPropagation();if(btndown) 97 | {btndown=false;onDone(mouseAbs(e));options.onSelect(unscale(Coords.getFixed()));toBack();onMove=function(){};onDone=function(){};} 98 | return false;};function activateHandlers(move,done) 99 | {btndown=true;onMove=move;onDone=done;toFront();return false;};function setCursor(t){$trk.css('cursor',t);};$img.before($trk);return{activateHandlers:activateHandlers,setCursor:setCursor};}();var KeyManager=function() 100 | {var $keymgr=$('').css({position:'absolute',left:'-30px'}).keypress(parseKey).blur(onBlur),$keywrap=$('
').css({position:'absolute',overflow:'hidden'}).append($keymgr);function watchKeys() 101 | {if(options.keySupport) 102 | {$keymgr.show();$keymgr.focus();}};function onBlur(e) 103 | {$keymgr.hide();};function doNudge(e,x,y) 104 | {if(options.allowMove){Coords.moveOffset([x,y]);Selection.updateVisible();};e.preventDefault();e.stopPropagation();};function parseKey(e) 105 | {if(e.ctrlKey)return true;shift_down=e.shiftKey?true:false;var nudge=shift_down?10:1;switch(e.keyCode) 106 | {case 37:doNudge(e,-nudge,0);break;case 39:doNudge(e,nudge,0);break;case 38:doNudge(e,0,-nudge);break;case 40:doNudge(e,0,nudge);break;case 27:Selection.release();break;case 9:return true;} 107 | return nothing(e);};if(options.keySupport)$keywrap.insertBefore($img);return{watchKeys:watchKeys};}();function px(n){return''+parseInt(n)+'px';};function pct(n){return''+parseInt(n)+'%';};function cssClass(cl){return options.baseClass+'-'+cl;};function getPos(obj) 108 | {var pos=$(obj).offset();return[pos.left,pos.top];};function mouseAbs(e) 109 | {return[(e.pageX-docOffset[0]),(e.pageY-docOffset[1])];};function myCursor(type) 110 | {if(type!=lastcurs) 111 | {Tracker.setCursor(type);lastcurs=type;}};function startDragMode(mode,pos) 112 | {docOffset=getPos($img);Tracker.setCursor(mode=='move'?mode:mode+'-resize');if(mode=='move') 113 | return Tracker.activateHandlers(createMover(pos),doneSelect);var fc=Coords.getFixed();var opp=oppLockCorner(mode);var opc=Coords.getCorner(oppLockCorner(opp));Coords.setPressed(Coords.getCorner(opp));Coords.setCurrent(opc);Tracker.activateHandlers(dragmodeHandler(mode,fc),doneSelect);};function dragmodeHandler(mode,f) 114 | {return function(pos){if(!options.aspectRatio)switch(mode) 115 | {case'e':pos[1]=f.y2;break;case'w':pos[1]=f.y2;break;case'n':pos[0]=f.x2;break;case's':pos[0]=f.x2;break;} 116 | else switch(mode) 117 | {case'e':pos[1]=f.y+1;break;case'w':pos[1]=f.y+1;break;case'n':pos[0]=f.x+1;break;case's':pos[0]=f.x+1;break;} 118 | Coords.setCurrent(pos);Selection.update();};};function createMover(pos) 119 | {var lloc=pos;KeyManager.watchKeys();return function(pos) 120 | {Coords.moveOffset([pos[0]-lloc[0],pos[1]-lloc[1]]);lloc=pos;Selection.update();};};function oppLockCorner(ord) 121 | {switch(ord) 122 | {case'n':return'sw';case's':return'nw';case'e':return'nw';case'w':return'ne';case'ne':return'sw';case'nw':return'se';case'se':return'nw';case'sw':return'ne';};};function createDragger(ord) 123 | {return function(e){if(options.disabled)return false;if((ord=='move')&&!options.allowMove)return false;btndown=true;startDragMode(ord,mouseAbs(e));e.stopPropagation();e.preventDefault();return false;};};function presize($obj,w,h) 124 | {var nw=$obj.width(),nh=$obj.height();if((nw>w)&&w>0) 125 | {nw=w;nh=(w/$obj.width())*$obj.height();} 126 | if((nh>h)&&h>0) 127 | {nh=h;nw=(h/$obj.height())*$obj.width();} 128 | xscale=$obj.width()/nw;yscale=$obj.height()/nh;$obj.width(nw).height(nh);};function unscale(c) 129 | {return{x:parseInt(c.x*xscale),y:parseInt(c.y*yscale),x2:parseInt(c.x2*xscale),y2:parseInt(c.y2*yscale),w:parseInt(c.w*xscale),h:parseInt(c.h*yscale)};};function doneSelect(pos) 130 | {var c=Coords.getFixed();if(c.w>options.minSelect[0]&&c.h>options.minSelect[1]) 131 | {Selection.enableHandles();Selection.done();} 132 | else 133 | {Selection.release();} 134 | Tracker.setCursor(options.allowSelect?'crosshair':'default');};function newSelection(e) 135 | {if(options.disabled)return false;if(!options.allowSelect)return false;btndown=true;docOffset=getPos($img);Selection.disableHandles();myCursor('crosshair');var pos=mouseAbs(e);Coords.setPressed(pos);Tracker.activateHandlers(selectDrag,doneSelect);KeyManager.watchKeys();Selection.update();e.stopPropagation();e.preventDefault();return false;};function selectDrag(pos) 136 | {Coords.setCurrent(pos);Selection.update();};function newTracker() 137 | {var trk=$('
').addClass(cssClass('tracker'));$.browser.msie&&trk.css({opacity:0,backgroundColor:'white'});return trk;};function animateTo(a) 138 | {var x1=a[0]/xscale,y1=a[1]/yscale,x2=a[2]/xscale,y2=a[3]/yscale;if(animating)return;var animto=Coords.flipCoords(x1,y1,x2,y2);var c=Coords.getFixed();var animat=initcr=[c.x,c.y,c.x2,c.y2];var interv=options.animationDelay;var x=animat[0];var y=animat[1];var x2=animat[2];var y2=animat[3];var ix1=animto[0]-initcr[0];var iy1=animto[1]-initcr[1];var ix2=animto[2]-initcr[2];var iy2=animto[3]-initcr[3];var pcent=0;var velocity=options.swingSpeed;Selection.animMode(true);var animator=function() 139 | {return function() 140 | {pcent+=(100-pcent)/velocity;animat[0]=x+((pcent/100)*ix1);animat[1]=y+((pcent/100)*iy1);animat[2]=x2+((pcent/100)*ix2);animat[3]=y2+((pcent/100)*iy2);if(pcent<100)animateStart();else Selection.done();if(pcent>=99.8)pcent=100;setSelectRaw(animat);};}();function animateStart() 141 | {window.setTimeout(animator,interv);};animateStart();};function setSelect(rect) 142 | {setSelectRaw([rect[0]/xscale,rect[1]/yscale,rect[2]/xscale,rect[3]/yscale]);};function setSelectRaw(l) 143 | {Coords.setPressed([l[0],l[1]]);Coords.setCurrent([l[2],l[3]]);Selection.update();};function setOptions(opt) 144 | {if(typeof(opt)!='object')opt={};options=$.extend(options,opt);if(typeof(options.onChange)!=='function') 145 | options.onChange=function(){};if(typeof(options.onSelect)!=='function') 146 | options.onSelect=function(){};};function tellSelect() 147 | {return unscale(Coords.getFixed());};function tellScaled() 148 | {return Coords.getFixed();};function setOptionsNew(opt) 149 | {setOptions(opt);interfaceUpdate();};function disableCrop() 150 | {options.disabled=true;Selection.disableHandles();Selection.setCursor('default');Tracker.setCursor('default');};function enableCrop() 151 | {options.disabled=false;interfaceUpdate();};function cancelCrop() 152 | {Selection.done();Tracker.activateHandlers(null,null);};function destroy() 153 | {$div.remove();$origimg.show();};function interfaceUpdate(alt) 154 | {options.allowResize?alt?Selection.enableOnly():Selection.enableHandles():Selection.disableHandles();Tracker.setCursor(options.allowSelect?'crosshair':'default');Selection.setCursor(options.allowMove?'move':'default');$div.css('backgroundColor',options.bgColor);if('setSelect'in options){setSelect(opt.setSelect);Selection.done();delete(options.setSelect);} 155 | if('trueSize'in options){xscale=options.trueSize[0]/boundx;yscale=options.trueSize[1]/boundy;} 156 | xlimit=options.maxSize[0]||0;ylimit=options.maxSize[1]||0;xmin=options.minSize[0]||0;ymin=options.minSize[1]||0;if('outerImage'in options) 157 | {$img.attr('src',options.outerImage);delete(options.outerImage);} 158 | Selection.refresh();};$hdl_holder.hide();interfaceUpdate(true);var api={animateTo:animateTo,setSelect:setSelect,setOptions:setOptionsNew,tellSelect:tellSelect,tellScaled:tellScaled,disable:disableCrop,enable:enableCrop,cancel:cancelCrop,focus:KeyManager.watchKeys,getBounds:function(){return[boundx*xscale,boundy*yscale];},getWidgetSize:function(){return[boundx,boundy];},release:Selection.release,destroy:destroy};$origimg.data('Jcrop',api);return api;};$.fn.Jcrop=function(options) 159 | {function attachWhenDone(from) 160 | {var loadsrc=options.useImg||from.src;var img=new Image();img.onload=function(){$.Jcrop(from,options);};img.src=loadsrc;};if(typeof(options)!=='object')options={};this.each(function() 161 | {if($(this).data('Jcrop')) 162 | {if(options=='api')return $(this).data('Jcrop');else $(this).data('Jcrop').setOptions(options);} 163 | else attachWhenDone(this);});return this;};})(jQuery); -------------------------------------------------------------------------------- /public/Jcrop/jquery.Jcrop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jquery.Jcrop.js v0.9.8 3 | * jQuery Image Cropping Plugin 4 | * @author Kelly Hallman 5 | * Copyright (c) 2008-2009 Kelly Hallman - released under MIT License {{{ 6 | * 7 | * Permission is hereby granted, free of charge, to any person 8 | * obtaining a copy of this software and associated documentation 9 | * files (the "Software"), to deal in the Software without 10 | * restriction, including without limitation the rights to use, 11 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the 13 | * Software is furnished to do so, subject to the following 14 | * conditions: 15 | 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | * OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | * }}} 29 | */ 30 | 31 | (function($) { 32 | 33 | $.Jcrop = function(obj,opt) 34 | { 35 | // Initialization {{{ 36 | 37 | // Sanitize some options {{{ 38 | var obj = obj, opt = opt; 39 | 40 | if (typeof(obj) !== 'object') obj = $(obj)[0]; 41 | if (typeof(opt) !== 'object') opt = { }; 42 | 43 | // Some on-the-fly fixes for MSIE...sigh 44 | if (!('trackDocument' in opt)) 45 | { 46 | opt.trackDocument = $.browser.msie ? false : true; 47 | if ($.browser.msie && $.browser.version.split('.')[0] == '8') 48 | opt.trackDocument = true; 49 | } 50 | 51 | if (!('keySupport' in opt)) 52 | opt.keySupport = $.browser.msie ? false : true; 53 | 54 | // }}} 55 | // Extend the default options {{{ 56 | var defaults = { 57 | 58 | // Basic Settings 59 | trackDocument: false, 60 | baseClass: 'jcrop', 61 | addClass: null, 62 | 63 | // Styling Options 64 | bgColor: 'black', 65 | bgOpacity: .6, 66 | borderOpacity: .4, 67 | handleOpacity: .5, 68 | 69 | handlePad: 5, 70 | handleSize: 9, 71 | handleOffset: 5, 72 | edgeMargin: 14, 73 | 74 | aspectRatio: 0, 75 | keySupport: true, 76 | cornerHandles: true, 77 | sideHandles: true, 78 | drawBorders: true, 79 | dragEdges: true, 80 | 81 | boxWidth: 0, 82 | boxHeight: 0, 83 | 84 | boundary: 8, 85 | animationDelay: 20, 86 | swingSpeed: 3, 87 | 88 | allowSelect: true, 89 | allowMove: true, 90 | allowResize: true, 91 | 92 | minSelect: [ 0, 0 ], 93 | maxSize: [ 0, 0 ], 94 | minSize: [ 0, 0 ], 95 | 96 | // Callbacks / Event Handlers 97 | onChange: function() { }, 98 | onSelect: function() { } 99 | 100 | }; 101 | var options = defaults; 102 | setOptions(opt); 103 | 104 | // }}} 105 | // Initialize some jQuery objects {{{ 106 | 107 | var $origimg = $(obj); 108 | var $img = $origimg.clone().removeAttr('id').css({ position: 'absolute' }); 109 | 110 | $img.width($origimg.width()); 111 | $img.height($origimg.height()); 112 | $origimg.after($img).hide(); 113 | 114 | presize($img,options.boxWidth,options.boxHeight); 115 | 116 | var boundx = $img.width(), 117 | boundy = $img.height(), 118 | 119 | $div = $('
') 120 | .width(boundx).height(boundy) 121 | .addClass(cssClass('holder')) 122 | .css({ 123 | position: 'relative', 124 | backgroundColor: options.bgColor 125 | }).insertAfter($origimg).append($img); 126 | ; 127 | 128 | if (options.addClass) $div.addClass(options.addClass); 129 | //$img.wrap($div); 130 | 131 | var $img2 = $('')/*{{{*/ 132 | .attr('src',$img.attr('src')) 133 | .css('position','absolute') 134 | .width(boundx).height(boundy) 135 | ;/*}}}*/ 136 | var $img_holder = $('
')/*{{{*/ 137 | .width(pct(100)).height(pct(100)) 138 | .css({ 139 | zIndex: 310, 140 | position: 'absolute', 141 | overflow: 'hidden' 142 | }) 143 | .append($img2) 144 | ;/*}}}*/ 145 | var $hdl_holder = $('
')/*{{{*/ 146 | .width(pct(100)).height(pct(100)) 147 | .css('zIndex',320); 148 | /*}}}*/ 149 | var $sel = $('
')/*{{{*/ 150 | .css({ 151 | position: 'absolute', 152 | zIndex: 300 153 | }) 154 | .insertBefore($img) 155 | .append($img_holder,$hdl_holder) 156 | ;/*}}}*/ 157 | 158 | var bound = options.boundary; 159 | var $trk = newTracker().width(boundx+(bound*2)).height(boundy+(bound*2)) 160 | .css({ position: 'absolute', top: px(-bound), left: px(-bound), zIndex: 290 }) 161 | .mousedown(newSelection); 162 | 163 | /* }}} */ 164 | // Set more variables {{{ 165 | 166 | var xlimit, ylimit, xmin, ymin; 167 | var xscale, yscale, enabled = true; 168 | var docOffset = getPos($img), 169 | // Internal states 170 | btndown, lastcurs, dimmed, animating, 171 | shift_down; 172 | 173 | // }}} 174 | 175 | 176 | // }}} 177 | // Internal Modules {{{ 178 | 179 | var Coords = function()/*{{{*/ 180 | { 181 | var x1 = 0, y1 = 0, x2 = 0, y2 = 0, ox, oy; 182 | 183 | function setPressed(pos)/*{{{*/ 184 | { 185 | var pos = rebound(pos); 186 | x2 = x1 = pos[0]; 187 | y2 = y1 = pos[1]; 188 | }; 189 | /*}}}*/ 190 | function setCurrent(pos)/*{{{*/ 191 | { 192 | var pos = rebound(pos); 193 | ox = pos[0] - x2; 194 | oy = pos[1] - y2; 195 | x2 = pos[0]; 196 | y2 = pos[1]; 197 | }; 198 | /*}}}*/ 199 | function getOffset()/*{{{*/ 200 | { 201 | return [ ox, oy ]; 202 | }; 203 | /*}}}*/ 204 | function moveOffset(offset)/*{{{*/ 205 | { 206 | var ox = offset[0], oy = offset[1]; 207 | 208 | if (0 > x1 + ox) ox -= ox + x1; 209 | if (0 > y1 + oy) oy -= oy + y1; 210 | 211 | if (boundy < y2 + oy) oy += boundy - (y2 + oy); 212 | if (boundx < x2 + ox) ox += boundx - (x2 + ox); 213 | 214 | x1 += ox; 215 | x2 += ox; 216 | y1 += oy; 217 | y2 += oy; 218 | }; 219 | /*}}}*/ 220 | function getCorner(ord)/*{{{*/ 221 | { 222 | var c = getFixed(); 223 | switch(ord) 224 | { 225 | case 'ne': return [ c.x2, c.y ]; 226 | case 'nw': return [ c.x, c.y ]; 227 | case 'se': return [ c.x2, c.y2 ]; 228 | case 'sw': return [ c.x, c.y2 ]; 229 | } 230 | }; 231 | /*}}}*/ 232 | function getFixed()/*{{{*/ 233 | { 234 | if (!options.aspectRatio) return getRect(); 235 | // This function could use some optimization I think... 236 | var aspect = options.aspectRatio, 237 | min_x = options.minSize[0]/xscale, 238 | min_y = options.minSize[1]/yscale, 239 | max_x = options.maxSize[0]/xscale, 240 | max_y = options.maxSize[1]/yscale, 241 | rw = x2 - x1, 242 | rh = y2 - y1, 243 | rwa = Math.abs(rw), 244 | rha = Math.abs(rh), 245 | real_ratio = rwa / rha, 246 | xx, yy 247 | ; 248 | if (max_x == 0) { max_x = boundx * 10 } 249 | if (max_y == 0) { max_y = boundy * 10 } 250 | if (real_ratio < aspect) 251 | { 252 | yy = y2; 253 | w = rha * aspect; 254 | xx = rw < 0 ? x1 - w : w + x1; 255 | 256 | if (xx < 0) 257 | { 258 | xx = 0; 259 | h = Math.abs((xx - x1) / aspect); 260 | yy = rh < 0 ? y1 - h: h + y1; 261 | } 262 | else if (xx > boundx) 263 | { 264 | xx = boundx; 265 | h = Math.abs((xx - x1) / aspect); 266 | yy = rh < 0 ? y1 - h : h + y1; 267 | } 268 | } 269 | else 270 | { 271 | xx = x2; 272 | h = rwa / aspect; 273 | yy = rh < 0 ? y1 - h : y1 + h; 274 | if (yy < 0) 275 | { 276 | yy = 0; 277 | w = Math.abs((yy - y1) * aspect); 278 | xx = rw < 0 ? x1 - w : w + x1; 279 | } 280 | else if (yy > boundy) 281 | { 282 | yy = boundy; 283 | w = Math.abs(yy - y1) * aspect; 284 | xx = rw < 0 ? x1 - w : w + x1; 285 | } 286 | } 287 | 288 | // Magic %-) 289 | if(xx > x1) { // right side 290 | if(xx - x1 < min_x) { 291 | xx = x1 + min_x; 292 | } else if (xx - x1 > max_x) { 293 | xx = x1 + max_x; 294 | } 295 | if(yy > y1) { 296 | yy = y1 + (xx - x1)/aspect; 297 | } else { 298 | yy = y1 - (xx - x1)/aspect; 299 | } 300 | } else if (xx < x1) { // left side 301 | if(x1 - xx < min_x) { 302 | xx = x1 - min_x 303 | } else if (x1 - xx > max_x) { 304 | xx = x1 - max_x; 305 | } 306 | if(yy > y1) { 307 | yy = y1 + (x1 - xx)/aspect; 308 | } else { 309 | yy = y1 - (x1 - xx)/aspect; 310 | } 311 | } 312 | 313 | if(xx < 0) { 314 | x1 -= xx; 315 | xx = 0; 316 | } else if (xx > boundx) { 317 | x1 -= xx - boundx; 318 | xx = boundx; 319 | } 320 | 321 | if(yy < 0) { 322 | y1 -= yy; 323 | yy = 0; 324 | } else if (yy > boundy) { 325 | y1 -= yy - boundy; 326 | yy = boundy; 327 | } 328 | 329 | return last = makeObj(flipCoords(x1,y1,xx,yy)); 330 | }; 331 | /*}}}*/ 332 | function rebound(p)/*{{{*/ 333 | { 334 | if (p[0] < 0) p[0] = 0; 335 | if (p[1] < 0) p[1] = 0; 336 | 337 | if (p[0] > boundx) p[0] = boundx; 338 | if (p[1] > boundy) p[1] = boundy; 339 | 340 | return [ p[0], p[1] ]; 341 | }; 342 | /*}}}*/ 343 | function flipCoords(x1,y1,x2,y2)/*{{{*/ 344 | { 345 | var xa = x1, xb = x2, ya = y1, yb = y2; 346 | if (x2 < x1) 347 | { 348 | xa = x2; 349 | xb = x1; 350 | } 351 | if (y2 < y1) 352 | { 353 | ya = y2; 354 | yb = y1; 355 | } 356 | return [ Math.round(xa), Math.round(ya), Math.round(xb), Math.round(yb) ]; 357 | }; 358 | /*}}}*/ 359 | function getRect()/*{{{*/ 360 | { 361 | var xsize = x2 - x1; 362 | var ysize = y2 - y1; 363 | 364 | if (xlimit && (Math.abs(xsize) > xlimit)) 365 | x2 = (xsize > 0) ? (x1 + xlimit) : (x1 - xlimit); 366 | if (ylimit && (Math.abs(ysize) > ylimit)) 367 | y2 = (ysize > 0) ? (y1 + ylimit) : (y1 - ylimit); 368 | 369 | if (ymin && (Math.abs(ysize) < ymin)) 370 | y2 = (ysize > 0) ? (y1 + ymin) : (y1 - ymin); 371 | if (xmin && (Math.abs(xsize) < xmin)) 372 | x2 = (xsize > 0) ? (x1 + xmin) : (x1 - xmin); 373 | 374 | if (x1 < 0) { x2 -= x1; x1 -= x1; } 375 | if (y1 < 0) { y2 -= y1; y1 -= y1; } 376 | if (x2 < 0) { x1 -= x2; x2 -= x2; } 377 | if (y2 < 0) { y1 -= y2; y2 -= y2; } 378 | if (x2 > boundx) { var delta = x2 - boundx; x1 -= delta; x2 -= delta; } 379 | if (y2 > boundy) { var delta = y2 - boundy; y1 -= delta; y2 -= delta; } 380 | if (x1 > boundx) { var delta = x1 - boundy; y2 -= delta; y1 -= delta; } 381 | if (y1 > boundy) { var delta = y1 - boundy; y2 -= delta; y1 -= delta; } 382 | 383 | return makeObj(flipCoords(x1,y1,x2,y2)); 384 | }; 385 | /*}}}*/ 386 | function makeObj(a)/*{{{*/ 387 | { 388 | return { x: a[0], y: a[1], x2: a[2], y2: a[3], 389 | w: a[2] - a[0], h: a[3] - a[1] }; 390 | }; 391 | /*}}}*/ 392 | 393 | return { 394 | flipCoords: flipCoords, 395 | setPressed: setPressed, 396 | setCurrent: setCurrent, 397 | getOffset: getOffset, 398 | moveOffset: moveOffset, 399 | getCorner: getCorner, 400 | getFixed: getFixed 401 | }; 402 | }(); 403 | 404 | /*}}}*/ 405 | var Selection = function()/*{{{*/ 406 | { 407 | var start, end, dragmode, awake, hdep = 370; 408 | var borders = { }; 409 | var handle = { }; 410 | var seehandles = false; 411 | var hhs = options.handleOffset; 412 | 413 | /* Insert draggable elements {{{*/ 414 | 415 | // Insert border divs for outline 416 | if (options.drawBorders) { 417 | borders = { 418 | top: insertBorder('hline') 419 | .css('top',$.browser.msie?px(-1):px(0)), 420 | bottom: insertBorder('hline'), 421 | left: insertBorder('vline'), 422 | right: insertBorder('vline') 423 | }; 424 | } 425 | 426 | // Insert handles on edges 427 | if (options.dragEdges) { 428 | handle.t = insertDragbar('n'); 429 | handle.b = insertDragbar('s'); 430 | handle.r = insertDragbar('e'); 431 | handle.l = insertDragbar('w'); 432 | } 433 | 434 | // Insert side handles 435 | options.sideHandles && 436 | createHandles(['n','s','e','w']); 437 | 438 | // Insert corner handles 439 | options.cornerHandles && 440 | createHandles(['sw','nw','ne','se']); 441 | 442 | /*}}}*/ 443 | // Private Methods 444 | function insertBorder(type)/*{{{*/ 445 | { 446 | var jq = $('
') 447 | .css({position: 'absolute', opacity: options.borderOpacity }) 448 | .addClass(cssClass(type)); 449 | $img_holder.append(jq); 450 | return jq; 451 | }; 452 | /*}}}*/ 453 | function dragDiv(ord,zi)/*{{{*/ 454 | { 455 | var jq = $('
') 456 | .mousedown(createDragger(ord)) 457 | .css({ 458 | cursor: ord+'-resize', 459 | position: 'absolute', 460 | zIndex: zi 461 | }) 462 | ; 463 | $hdl_holder.append(jq); 464 | return jq; 465 | }; 466 | /*}}}*/ 467 | function insertHandle(ord)/*{{{*/ 468 | { 469 | return dragDiv(ord,hdep++) 470 | .css({ top: px(-hhs+1), left: px(-hhs+1), opacity: options.handleOpacity }) 471 | .addClass(cssClass('handle')); 472 | }; 473 | /*}}}*/ 474 | function insertDragbar(ord)/*{{{*/ 475 | { 476 | var s = options.handleSize, 477 | o = hhs, 478 | h = s, w = s, 479 | t = o, l = o; 480 | 481 | switch(ord) 482 | { 483 | case 'n': case 's': w = pct(100); break; 484 | case 'e': case 'w': h = pct(100); break; 485 | } 486 | 487 | return dragDiv(ord,hdep++).width(w).height(h) 488 | .css({ top: px(-t+1), left: px(-l+1)}); 489 | }; 490 | /*}}}*/ 491 | function createHandles(li)/*{{{*/ 492 | { 493 | for(i in li) handle[li[i]] = insertHandle(li[i]); 494 | }; 495 | /*}}}*/ 496 | function moveHandles(c)/*{{{*/ 497 | { 498 | var midvert = Math.round((c.h / 2) - hhs), 499 | midhoriz = Math.round((c.w / 2) - hhs), 500 | north = west = -hhs+1, 501 | east = c.w - hhs, 502 | south = c.h - hhs, 503 | x, y; 504 | 505 | 'e' in handle && 506 | handle.e.css({ top: px(midvert), left: px(east) }) && 507 | handle.w.css({ top: px(midvert) }) && 508 | handle.s.css({ top: px(south), left: px(midhoriz) }) && 509 | handle.n.css({ left: px(midhoriz) }); 510 | 511 | 'ne' in handle && 512 | handle.ne.css({ left: px(east) }) && 513 | handle.se.css({ top: px(south), left: px(east) }) && 514 | handle.sw.css({ top: px(south) }); 515 | 516 | 'b' in handle && 517 | handle.b.css({ top: px(south) }) && 518 | handle.r.css({ left: px(east) }); 519 | }; 520 | /*}}}*/ 521 | function moveto(x,y)/*{{{*/ 522 | { 523 | $img2.css({ top: px(-y), left: px(-x) }); 524 | $sel.css({ top: px(y), left: px(x) }); 525 | }; 526 | /*}}}*/ 527 | function resize(w,h)/*{{{*/ 528 | { 529 | $sel.width(w).height(h); 530 | }; 531 | /*}}}*/ 532 | function refresh()/*{{{*/ 533 | { 534 | var c = Coords.getFixed(); 535 | 536 | Coords.setPressed([c.x,c.y]); 537 | Coords.setCurrent([c.x2,c.y2]); 538 | 539 | updateVisible(); 540 | }; 541 | /*}}}*/ 542 | 543 | // Internal Methods 544 | function updateVisible()/*{{{*/ 545 | { if (awake) return update(); }; 546 | /*}}}*/ 547 | function update()/*{{{*/ 548 | { 549 | var c = Coords.getFixed(); 550 | 551 | resize(c.w,c.h); 552 | moveto(c.x,c.y); 553 | 554 | options.drawBorders && 555 | borders['right'].css({ left: px(c.w-1) }) && 556 | borders['bottom'].css({ top: px(c.h-1) }); 557 | 558 | seehandles && moveHandles(c); 559 | awake || show(); 560 | 561 | options.onChange(unscale(c)); 562 | }; 563 | /*}}}*/ 564 | function show()/*{{{*/ 565 | { 566 | $sel.show(); 567 | $img.css('opacity',options.bgOpacity); 568 | awake = true; 569 | }; 570 | /*}}}*/ 571 | function release()/*{{{*/ 572 | { 573 | disableHandles(); 574 | $sel.hide(); 575 | $img.css('opacity',1); 576 | awake = false; 577 | }; 578 | /*}}}*/ 579 | function showHandles()//{{{ 580 | { 581 | if (seehandles) 582 | { 583 | moveHandles(Coords.getFixed()); 584 | $hdl_holder.show(); 585 | } 586 | }; 587 | //}}} 588 | function enableHandles()/*{{{*/ 589 | { 590 | seehandles = true; 591 | if (options.allowResize) 592 | { 593 | moveHandles(Coords.getFixed()); 594 | $hdl_holder.show(); 595 | return true; 596 | } 597 | }; 598 | /*}}}*/ 599 | function disableHandles()/*{{{*/ 600 | { 601 | seehandles = false; 602 | $hdl_holder.hide(); 603 | }; 604 | /*}}}*/ 605 | function animMode(v)/*{{{*/ 606 | { 607 | (animating = v) ? disableHandles(): enableHandles(); 608 | }; 609 | /*}}}*/ 610 | function done()/*{{{*/ 611 | { 612 | animMode(false); 613 | refresh(); 614 | }; 615 | /*}}}*/ 616 | 617 | var $track = newTracker().mousedown(createDragger('move')) 618 | .css({ cursor: 'move', position: 'absolute', zIndex: 360 }) 619 | 620 | $img_holder.append($track); 621 | disableHandles(); 622 | 623 | return { 624 | updateVisible: updateVisible, 625 | update: update, 626 | release: release, 627 | refresh: refresh, 628 | setCursor: function (cursor) { $track.css('cursor',cursor); }, 629 | enableHandles: enableHandles, 630 | enableOnly: function() { seehandles = true; }, 631 | showHandles: showHandles, 632 | disableHandles: disableHandles, 633 | animMode: animMode, 634 | done: done 635 | }; 636 | }(); 637 | /*}}}*/ 638 | var Tracker = function()/*{{{*/ 639 | { 640 | var onMove = function() { }, 641 | onDone = function() { }, 642 | trackDoc = options.trackDocument; 643 | 644 | if (!trackDoc) 645 | { 646 | $trk 647 | .mousemove(trackMove) 648 | .mouseup(trackUp) 649 | .mouseout(trackUp) 650 | ; 651 | } 652 | 653 | function toFront()/*{{{*/ 654 | { 655 | $trk.css({zIndex:450}); 656 | if (trackDoc) 657 | { 658 | $(document) 659 | .mousemove(trackMove) 660 | .mouseup(trackUp) 661 | ; 662 | } 663 | } 664 | /*}}}*/ 665 | function toBack()/*{{{*/ 666 | { 667 | $trk.css({zIndex:290}); 668 | if (trackDoc) 669 | { 670 | $(document) 671 | .unbind('mousemove',trackMove) 672 | .unbind('mouseup',trackUp) 673 | ; 674 | } 675 | } 676 | /*}}}*/ 677 | function trackMove(e)/*{{{*/ 678 | { 679 | onMove(mouseAbs(e)); 680 | }; 681 | /*}}}*/ 682 | function trackUp(e)/*{{{*/ 683 | { 684 | e.preventDefault(); 685 | e.stopPropagation(); 686 | 687 | if (btndown) 688 | { 689 | btndown = false; 690 | 691 | onDone(mouseAbs(e)); 692 | options.onSelect(unscale(Coords.getFixed())); 693 | toBack(); 694 | onMove = function() { }; 695 | onDone = function() { }; 696 | } 697 | 698 | return false; 699 | }; 700 | /*}}}*/ 701 | 702 | function activateHandlers(move,done)/* {{{ */ 703 | { 704 | btndown = true; 705 | onMove = move; 706 | onDone = done; 707 | toFront(); 708 | return false; 709 | }; 710 | /* }}} */ 711 | 712 | function setCursor(t) { $trk.css('cursor',t); }; 713 | 714 | $img.before($trk); 715 | return { 716 | activateHandlers: activateHandlers, 717 | setCursor: setCursor 718 | }; 719 | }(); 720 | /*}}}*/ 721 | var KeyManager = function()/*{{{*/ 722 | { 723 | var $keymgr = $('') 724 | .css({ position: 'absolute', left: '-30px' }) 725 | .keypress(parseKey) 726 | .blur(onBlur), 727 | 728 | $keywrap = $('
') 729 | .css({ 730 | position: 'absolute', 731 | overflow: 'hidden' 732 | }) 733 | .append($keymgr) 734 | ; 735 | 736 | function watchKeys()/*{{{*/ 737 | { 738 | if (options.keySupport) 739 | { 740 | $keymgr.show(); 741 | $keymgr.focus(); 742 | } 743 | }; 744 | /*}}}*/ 745 | function onBlur(e)/*{{{*/ 746 | { 747 | $keymgr.hide(); 748 | }; 749 | /*}}}*/ 750 | function doNudge(e,x,y)/*{{{*/ 751 | { 752 | if (options.allowMove) { 753 | Coords.moveOffset([x,y]); 754 | Selection.updateVisible(); 755 | }; 756 | e.preventDefault(); 757 | e.stopPropagation(); 758 | }; 759 | /*}}}*/ 760 | function parseKey(e)/*{{{*/ 761 | { 762 | if (e.ctrlKey) return true; 763 | shift_down = e.shiftKey ? true : false; 764 | var nudge = shift_down ? 10 : 1; 765 | switch(e.keyCode) 766 | { 767 | case 37: doNudge(e,-nudge,0); break; 768 | case 39: doNudge(e,nudge,0); break; 769 | case 38: doNudge(e,0,-nudge); break; 770 | case 40: doNudge(e,0,nudge); break; 771 | 772 | case 27: Selection.release(); break; 773 | 774 | case 9: return true; 775 | } 776 | 777 | return nothing(e); 778 | }; 779 | /*}}}*/ 780 | 781 | if (options.keySupport) $keywrap.insertBefore($img); 782 | return { 783 | watchKeys: watchKeys 784 | }; 785 | }(); 786 | /*}}}*/ 787 | 788 | // }}} 789 | // Internal Methods {{{ 790 | 791 | function px(n) { return '' + parseInt(n) + 'px'; }; 792 | function pct(n) { return '' + parseInt(n) + '%'; }; 793 | function cssClass(cl) { return options.baseClass + '-' + cl; }; 794 | function getPos(obj)/*{{{*/ 795 | { 796 | // Updated in v0.9.4 to use built-in dimensions plugin 797 | var pos = $(obj).offset(); 798 | return [ pos.left, pos.top ]; 799 | }; 800 | /*}}}*/ 801 | function mouseAbs(e)/*{{{*/ 802 | { 803 | return [ (e.pageX - docOffset[0]), (e.pageY - docOffset[1]) ]; 804 | }; 805 | /*}}}*/ 806 | function myCursor(type)/*{{{*/ 807 | { 808 | if (type != lastcurs) 809 | { 810 | Tracker.setCursor(type); 811 | //Handles.xsetCursor(type); 812 | lastcurs = type; 813 | } 814 | }; 815 | /*}}}*/ 816 | function startDragMode(mode,pos)/*{{{*/ 817 | { 818 | docOffset = getPos($img); 819 | Tracker.setCursor(mode=='move'?mode:mode+'-resize'); 820 | 821 | if (mode == 'move') 822 | return Tracker.activateHandlers(createMover(pos), doneSelect); 823 | 824 | var fc = Coords.getFixed(); 825 | var opp = oppLockCorner(mode); 826 | var opc = Coords.getCorner(oppLockCorner(opp)); 827 | 828 | Coords.setPressed(Coords.getCorner(opp)); 829 | Coords.setCurrent(opc); 830 | 831 | Tracker.activateHandlers(dragmodeHandler(mode,fc),doneSelect); 832 | }; 833 | /*}}}*/ 834 | function dragmodeHandler(mode,f)/*{{{*/ 835 | { 836 | return function(pos) { 837 | if (!options.aspectRatio) switch(mode) 838 | { 839 | case 'e': pos[1] = f.y2; break; 840 | case 'w': pos[1] = f.y2; break; 841 | case 'n': pos[0] = f.x2; break; 842 | case 's': pos[0] = f.x2; break; 843 | } 844 | else switch(mode) 845 | { 846 | case 'e': pos[1] = f.y+1; break; 847 | case 'w': pos[1] = f.y+1; break; 848 | case 'n': pos[0] = f.x+1; break; 849 | case 's': pos[0] = f.x+1; break; 850 | } 851 | Coords.setCurrent(pos); 852 | Selection.update(); 853 | }; 854 | }; 855 | /*}}}*/ 856 | function createMover(pos)/*{{{*/ 857 | { 858 | var lloc = pos; 859 | KeyManager.watchKeys(); 860 | 861 | return function(pos) 862 | { 863 | Coords.moveOffset([pos[0] - lloc[0], pos[1] - lloc[1]]); 864 | lloc = pos; 865 | 866 | Selection.update(); 867 | }; 868 | }; 869 | /*}}}*/ 870 | function oppLockCorner(ord)/*{{{*/ 871 | { 872 | switch(ord) 873 | { 874 | case 'n': return 'sw'; 875 | case 's': return 'nw'; 876 | case 'e': return 'nw'; 877 | case 'w': return 'ne'; 878 | case 'ne': return 'sw'; 879 | case 'nw': return 'se'; 880 | case 'se': return 'nw'; 881 | case 'sw': return 'ne'; 882 | }; 883 | }; 884 | /*}}}*/ 885 | function createDragger(ord)/*{{{*/ 886 | { 887 | return function(e) { 888 | if (options.disabled) return false; 889 | if ((ord == 'move') && !options.allowMove) return false; 890 | btndown = true; 891 | startDragMode(ord,mouseAbs(e)); 892 | e.stopPropagation(); 893 | e.preventDefault(); 894 | return false; 895 | }; 896 | }; 897 | /*}}}*/ 898 | function presize($obj,w,h)/*{{{*/ 899 | { 900 | var nw = $obj.width(), nh = $obj.height(); 901 | if ((nw > w) && w > 0) 902 | { 903 | nw = w; 904 | nh = (w/$obj.width()) * $obj.height(); 905 | } 906 | if ((nh > h) && h > 0) 907 | { 908 | nh = h; 909 | nw = (h/$obj.height()) * $obj.width(); 910 | } 911 | xscale = $obj.width() / nw; 912 | yscale = $obj.height() / nh; 913 | $obj.width(nw).height(nh); 914 | }; 915 | /*}}}*/ 916 | function unscale(c)/*{{{*/ 917 | { 918 | return { 919 | x: parseInt(c.x * xscale), y: parseInt(c.y * yscale), 920 | x2: parseInt(c.x2 * xscale), y2: parseInt(c.y2 * yscale), 921 | w: parseInt(c.w * xscale), h: parseInt(c.h * yscale) 922 | }; 923 | }; 924 | /*}}}*/ 925 | function doneSelect(pos)/*{{{*/ 926 | { 927 | var c = Coords.getFixed(); 928 | if (c.w > options.minSelect[0] && c.h > options.minSelect[1]) 929 | { 930 | Selection.enableHandles(); 931 | Selection.done(); 932 | } 933 | else 934 | { 935 | Selection.release(); 936 | } 937 | Tracker.setCursor( options.allowSelect?'crosshair':'default' ); 938 | }; 939 | /*}}}*/ 940 | function newSelection(e)/*{{{*/ 941 | { 942 | if (options.disabled) return false; 943 | if (!options.allowSelect) return false; 944 | btndown = true; 945 | docOffset = getPos($img); 946 | Selection.disableHandles(); 947 | myCursor('crosshair'); 948 | var pos = mouseAbs(e); 949 | Coords.setPressed(pos); 950 | Tracker.activateHandlers(selectDrag,doneSelect); 951 | KeyManager.watchKeys(); 952 | Selection.update(); 953 | 954 | e.stopPropagation(); 955 | e.preventDefault(); 956 | return false; 957 | }; 958 | /*}}}*/ 959 | function selectDrag(pos)/*{{{*/ 960 | { 961 | Coords.setCurrent(pos); 962 | Selection.update(); 963 | }; 964 | /*}}}*/ 965 | function newTracker() 966 | { 967 | var trk = $('
').addClass(cssClass('tracker')); 968 | $.browser.msie && trk.css({ opacity: 0, backgroundColor: 'white' }); 969 | return trk; 970 | }; 971 | 972 | // }}} 973 | // API methods {{{ 974 | 975 | function animateTo(a)/*{{{*/ 976 | { 977 | var x1 = a[0] / xscale, 978 | y1 = a[1] / yscale, 979 | x2 = a[2] / xscale, 980 | y2 = a[3] / yscale; 981 | 982 | if (animating) return; 983 | 984 | var animto = Coords.flipCoords(x1,y1,x2,y2); 985 | var c = Coords.getFixed(); 986 | var animat = initcr = [ c.x, c.y, c.x2, c.y2 ]; 987 | var interv = options.animationDelay; 988 | 989 | var x = animat[0]; 990 | var y = animat[1]; 991 | var x2 = animat[2]; 992 | var y2 = animat[3]; 993 | var ix1 = animto[0] - initcr[0]; 994 | var iy1 = animto[1] - initcr[1]; 995 | var ix2 = animto[2] - initcr[2]; 996 | var iy2 = animto[3] - initcr[3]; 997 | var pcent = 0; 998 | var velocity = options.swingSpeed; 999 | 1000 | Selection.animMode(true); 1001 | 1002 | var animator = function() 1003 | { 1004 | return function() 1005 | { 1006 | pcent += (100 - pcent) / velocity; 1007 | 1008 | animat[0] = x + ((pcent / 100) * ix1); 1009 | animat[1] = y + ((pcent / 100) * iy1); 1010 | animat[2] = x2 + ((pcent / 100) * ix2); 1011 | animat[3] = y2 + ((pcent / 100) * iy2); 1012 | 1013 | if (pcent < 100) animateStart(); 1014 | else Selection.done(); 1015 | 1016 | if (pcent >= 99.8) pcent = 100; 1017 | 1018 | setSelectRaw(animat); 1019 | }; 1020 | }(); 1021 | 1022 | function animateStart() 1023 | { window.setTimeout(animator,interv); }; 1024 | 1025 | animateStart(); 1026 | }; 1027 | /*}}}*/ 1028 | function setSelect(rect)//{{{ 1029 | { 1030 | setSelectRaw([rect[0]/xscale,rect[1]/yscale,rect[2]/xscale,rect[3]/yscale]); 1031 | }; 1032 | //}}} 1033 | function setSelectRaw(l) /*{{{*/ 1034 | { 1035 | Coords.setPressed([l[0],l[1]]); 1036 | Coords.setCurrent([l[2],l[3]]); 1037 | Selection.update(); 1038 | }; 1039 | /*}}}*/ 1040 | function setOptions(opt)/*{{{*/ 1041 | { 1042 | if (typeof(opt) != 'object') opt = { }; 1043 | options = $.extend(options,opt); 1044 | 1045 | if (typeof(options.onChange)!=='function') 1046 | options.onChange = function() { }; 1047 | 1048 | if (typeof(options.onSelect)!=='function') 1049 | options.onSelect = function() { }; 1050 | 1051 | }; 1052 | /*}}}*/ 1053 | function tellSelect()/*{{{*/ 1054 | { 1055 | return unscale(Coords.getFixed()); 1056 | }; 1057 | /*}}}*/ 1058 | function tellScaled()/*{{{*/ 1059 | { 1060 | return Coords.getFixed(); 1061 | }; 1062 | /*}}}*/ 1063 | function setOptionsNew(opt)/*{{{*/ 1064 | { 1065 | setOptions(opt); 1066 | interfaceUpdate(); 1067 | }; 1068 | /*}}}*/ 1069 | function disableCrop()//{{{ 1070 | { 1071 | options.disabled = true; 1072 | Selection.disableHandles(); 1073 | Selection.setCursor('default'); 1074 | Tracker.setCursor('default'); 1075 | }; 1076 | //}}} 1077 | function enableCrop()//{{{ 1078 | { 1079 | options.disabled = false; 1080 | interfaceUpdate(); 1081 | }; 1082 | //}}} 1083 | function cancelCrop()//{{{ 1084 | { 1085 | Selection.done(); 1086 | Tracker.activateHandlers(null,null); 1087 | }; 1088 | //}}} 1089 | function destroy()//{{{ 1090 | { 1091 | $div.remove(); 1092 | $origimg.show(); 1093 | }; 1094 | //}}} 1095 | 1096 | function interfaceUpdate(alt)//{{{ 1097 | // This method tweaks the interface based on options object. 1098 | // Called when options are changed and at end of initialization. 1099 | { 1100 | options.allowResize ? 1101 | alt?Selection.enableOnly():Selection.enableHandles(): 1102 | Selection.disableHandles(); 1103 | 1104 | Tracker.setCursor( options.allowSelect? 'crosshair': 'default' ); 1105 | Selection.setCursor( options.allowMove? 'move': 'default' ); 1106 | 1107 | $div.css('backgroundColor',options.bgColor); 1108 | 1109 | if ('setSelect' in options) { 1110 | setSelect(opt.setSelect); 1111 | Selection.done(); 1112 | delete(options.setSelect); 1113 | } 1114 | 1115 | if ('trueSize' in options) { 1116 | xscale = options.trueSize[0] / boundx; 1117 | yscale = options.trueSize[1] / boundy; 1118 | } 1119 | 1120 | xlimit = options.maxSize[0] || 0; 1121 | ylimit = options.maxSize[1] || 0; 1122 | xmin = options.minSize[0] || 0; 1123 | ymin = options.minSize[1] || 0; 1124 | 1125 | if ('outerImage' in options) 1126 | { 1127 | $img.attr('src',options.outerImage); 1128 | delete(options.outerImage); 1129 | } 1130 | 1131 | Selection.refresh(); 1132 | }; 1133 | //}}} 1134 | 1135 | // }}} 1136 | 1137 | $hdl_holder.hide(); 1138 | interfaceUpdate(true); 1139 | 1140 | var api = { 1141 | animateTo: animateTo, 1142 | setSelect: setSelect, 1143 | setOptions: setOptionsNew, 1144 | tellSelect: tellSelect, 1145 | tellScaled: tellScaled, 1146 | 1147 | disable: disableCrop, 1148 | enable: enableCrop, 1149 | cancel: cancelCrop, 1150 | 1151 | focus: KeyManager.watchKeys, 1152 | 1153 | getBounds: function() { return [ boundx * xscale, boundy * yscale ]; }, 1154 | getWidgetSize: function() { return [ boundx, boundy ]; }, 1155 | 1156 | release: Selection.release, 1157 | destroy: destroy 1158 | 1159 | }; 1160 | 1161 | $origimg.data('Jcrop',api); 1162 | return api; 1163 | }; 1164 | 1165 | $.fn.Jcrop = function(options)/*{{{*/ 1166 | { 1167 | function attachWhenDone(from)/*{{{*/ 1168 | { 1169 | var loadsrc = options.useImg || from.src; 1170 | var img = new Image(); 1171 | img.onload = function() { $.Jcrop(from,options); }; 1172 | img.src = loadsrc; 1173 | }; 1174 | /*}}}*/ 1175 | if (typeof(options) !== 'object') options = { }; 1176 | 1177 | // Iterate over each object, attach Jcrop 1178 | this.each(function() 1179 | { 1180 | // If we've already attached to this object 1181 | if ($(this).data('Jcrop')) 1182 | { 1183 | // The API can be requested this way (undocumented) 1184 | if (options == 'api') return $(this).data('Jcrop'); 1185 | // Otherwise, we just reset the options... 1186 | else $(this).data('Jcrop').setOptions(options); 1187 | } 1188 | // If we haven't been attached, preload and attach 1189 | else attachWhenDone(this); 1190 | }); 1191 | 1192 | // Return "this" so we're chainable a la jQuery plugin-style! 1193 | return this; 1194 | }; 1195 | /*}}}*/ 1196 | 1197 | })(jQuery); 1198 | --------------------------------------------------------------------------------