├── lib
├── .gitignore
├── tolk
│ ├── version.rb
│ ├── engine.rb
│ ├── import.rb
│ ├── config.rb
│ └── sync.rb
├── tolk.rb
├── generators
│ └── tolk
│ │ ├── templates
│ │ ├── initializer.erb
│ │ └── migration.rb
│ │ ├── utils.rb
│ │ └── install_generator.rb
└── tasks
│ └── tolk_tasks.rake
├── public
├── favicon.ico
├── images
│ └── rails.png
├── javascripts
│ ├── application.js
│ └── dragdrop.js
├── robots.txt
├── dispatch.cgi
├── dispatch.rb
├── dispatch.fcgi
├── 422.html
├── 404.html
└── 500.html
├── log
└── .gitignore
├── vendor
├── .gitignore
└── plugins
│ └── .gitignore
├── db
├── .gitignore
├── migrate
│ └── 20110824135256_migrate_tolk.rb
└── schema.rb
├── test
├── dummy
│ ├── db
│ │ ├── migrate
│ │ │ └── .gitkeep
│ │ ├── development.sqlite3
│ │ └── schema.rb
│ ├── public
│ │ ├── favicon.ico
│ │ ├── stylesheets
│ │ │ └── .gitkeep
│ │ ├── javascripts
│ │ │ ├── application.js
│ │ │ ├── rails.js
│ │ │ └── dragdrop.js
│ │ ├── 422.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── config
│ │ ├── locales
│ │ │ └── sync
│ │ │ │ └── .gitkeep
│ │ ├── routes.rb
│ │ ├── environment.rb
│ │ ├── initializers
│ │ │ ├── mime_types.rb
│ │ │ ├── inflections.rb
│ │ │ ├── session_store.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ └── secret_token.rb
│ │ ├── boot.rb
│ │ ├── database.yml
│ │ ├── environments
│ │ │ ├── development.rb
│ │ │ ├── test.rb
│ │ │ └── production.rb
│ │ └── application.rb
│ ├── app
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── controllers
│ │ │ └── application_controller.rb
│ │ └── views
│ │ │ └── layouts
│ │ │ └── application.html.erb
│ ├── config.ru
│ ├── Rakefile
│ └── script
│ │ └── rails
├── locales
│ ├── basic
│ │ ├── se.yml
│ │ ├── da.yml
│ │ └── en.yml
│ ├── import
│ │ ├── gem.en.yml
│ │ └── en.yml
│ ├── sync
│ │ └── en.yml
│ └── formats
│ │ └── en.yml
├── fixtures
│ ├── tolk_locales.yml
│ ├── tolk_phrases.yml
│ └── tolk_translations.yml
├── support
│ ├── integration_case.rb
│ ├── test_case.rb
│ └── active_record_shared_connections.rb
├── unit
│ ├── config_test.rb
│ ├── import_test.rb
│ ├── translation_test.rb
│ ├── locale_test.rb
│ ├── format_test.rb
│ └── sync_test.rb
├── integration
│ ├── authentication_test.rb
│ └── translation_process_test.rb
└── test_helper.rb
├── app
├── assets
│ ├── stylesheets
│ │ └── tolk
│ │ │ ├── application.css
│ │ │ ├── reset.css
│ │ │ └── screen.css
│ └── javascripts
│ │ └── tolk
│ │ └── application.js
├── views
│ ├── tolk
│ │ ├── searches
│ │ │ ├── _form.html.erb
│ │ │ └── show.html.erb
│ │ └── locales
│ │ │ ├── show.atom.builder
│ │ │ ├── index.html.erb
│ │ │ ├── all.html.erb
│ │ │ └── show.html.erb
│ └── layouts
│ │ └── tolk
│ │ └── application.html.erb
├── controllers
│ └── tolk
│ │ ├── searches_controller.rb
│ │ ├── application_controller.rb
│ │ └── locales_controller.rb
├── models
│ └── tolk
│ │ ├── phrase.rb
│ │ ├── translation.rb
│ │ └── locale.rb
└── helpers
│ └── tolk
│ └── application_helper.rb
├── .gitignore
├── .travis.yml
├── doc
├── DEPLOY.md
└── README_FOR_APP
├── init.rb
├── config
└── routes.rb
├── script
└── rails
├── Gemfile
├── CHANGELOG.md
├── tolk.gemspec
├── MIT-LICENSE
├── Rakefile
└── README.md
/lib/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/log/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
--------------------------------------------------------------------------------
/vendor/.gitignore:
--------------------------------------------------------------------------------
1 | rails
--------------------------------------------------------------------------------
/db/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite3
2 |
--------------------------------------------------------------------------------
/vendor/plugins/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/stylesheets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/config/locales/sync/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tolk/version.rb:
--------------------------------------------------------------------------------
1 | module Tolk
2 | VERSION = "1.3.2"
3 | end
--------------------------------------------------------------------------------
/test/locales/basic/se.yml:
--------------------------------------------------------------------------------
1 | ---
2 | se:
3 | hello_world: "Hejsan Verdon"
4 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/test/locales/import/gem.en.yml:
--------------------------------------------------------------------------------
1 | ---
2 | en:
3 | gem_hello_world: Gem Hello World
4 |
--------------------------------------------------------------------------------
/public/images/rails.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hughevans/tolk/master/public/images/rails.png
--------------------------------------------------------------------------------
/app/assets/stylesheets/tolk/application.css:
--------------------------------------------------------------------------------
1 | # =require 'tolk/reset.css'
2 | # =require 'tolk/screen.css'
3 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.routes.draw do
2 | mount Tolk::Engine => "/tolk"
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/db/development.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hughevans/tolk/master/test/dummy/db/development.sqlite3
--------------------------------------------------------------------------------
/lib/tolk/engine.rb:
--------------------------------------------------------------------------------
1 | require 'rails'
2 |
3 | module Tolk
4 | class Engine < Rails::Engine
5 | isolate_namespace Tolk
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 | end
4 |
--------------------------------------------------------------------------------
/test/fixtures/tolk_locales.yml:
--------------------------------------------------------------------------------
1 | en:
2 | id: 1
3 | name: en
4 |
5 | da:
6 | id: 2
7 | name: da
8 |
9 | se:
10 | id: 3
11 | name: se
12 |
--------------------------------------------------------------------------------
/test/locales/basic/da.yml:
--------------------------------------------------------------------------------
1 | ---
2 | da:
3 | cozy: Hyggeligt
4 | hello_world: "Hej Verden"
5 | nested:
6 | hello_world: "Nedarvet Hej Verden"
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | tmp/**/*
2 | test/dummy/db/migrate/*.tolk.rb
3 | test/dummy/log/*
4 | test/dummy/tmp/*
5 | test/dummy/db/*.sqlite3
6 | Gemfile.lock
7 | .bundle
8 | *.gem
--------------------------------------------------------------------------------
/test/locales/basic/en.yml:
--------------------------------------------------------------------------------
1 | ---
2 | en:
3 | nested:
4 | hello_world: Nested Hello World
5 | hello_country: Nested Hello Country
6 | hello_world: Hello World
7 |
--------------------------------------------------------------------------------
/app/assets/javascripts/tolk/application.js:
--------------------------------------------------------------------------------
1 | //= require tolk/prototype.js
2 | //= require tolk/controls.js
3 | //= require tolk/dragdrop.js
4 | //= require tolk/effects.js
5 |
--------------------------------------------------------------------------------
/test/locales/import/en.yml:
--------------------------------------------------------------------------------
1 | ---
2 | en:
3 | nested:
4 | hello_world: Nested Hello World
5 | hello_country: Nested Hello Country
6 | hello_world: Hello World
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | bundler_args: --without development
3 | before_script:
4 | - "export DISPLAY=:99.0"
5 | - "sh -e /etc/init.d/xvfb start"
6 | rvm:
7 | - 1.9.3
8 |
--------------------------------------------------------------------------------
/public/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // Place your application-specific JavaScript functions and classes here
2 | // This file is automatically included by javascript_include_tag :defaults
3 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Dummy::Application
5 |
--------------------------------------------------------------------------------
/test/locales/sync/en.yml:
--------------------------------------------------------------------------------
1 | ---
2 | en:
3 | nested:
4 | hello_country: Nested Hello Country
5 | hello_world: Hello World
6 | i18n:
7 | plural: Locale specific pluralization rules
8 |
--------------------------------------------------------------------------------
/doc/DEPLOY.md:
--------------------------------------------------------------------------------
1 | # Rubygem deployment
2 |
3 | * Update version in version.rb
4 | * Update changelog in version.rb
5 | * Tag Repository in git
6 | * `gem build tolk.gemspec`
7 | * `gem push tolk-1.X.0.gem`
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | Dummy::Application.initialize!
6 |
--------------------------------------------------------------------------------
/test/dummy/public/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // Place your application-specific JavaScript functions and classes here
2 | // This file is automatically included by javascript_include_tag :defaults
3 |
--------------------------------------------------------------------------------
/init.rb:
--------------------------------------------------------------------------------
1 | Mime::Type.register_alias "text/yaml", :yml
2 |
3 | $KCODE = 'UTF8'
4 | begin
5 | require 'ya2yaml'
6 | rescue LoadError => e
7 | Rails.logger.debug "[Tolk] Could not load ya2yaml"
8 | end
9 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Tolk::Engine.routes.draw do
2 | root :to => 'locales#index'
3 | resources :locales do
4 | member do
5 | get :all
6 | get :updated
7 | end
8 | end
9 | resource :search
10 | end
11 |
--------------------------------------------------------------------------------
/doc/README_FOR_APP:
--------------------------------------------------------------------------------
1 | Use this README file to introduce your application and point to useful places in the API for learning more.
2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries.
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-Agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/test/support/integration_case.rb:
--------------------------------------------------------------------------------
1 | # Define a bare test case to use with Capybara
2 | class ActiveSupport::IntegrationCase < ActiveSupport::TestCase
3 | include Capybara::DSL
4 | include Rails.application.routes.url_helpers
5 | include Rails.application.routes.mounted_helpers
6 | end
7 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3 |
4 | if File.exist?(gemfile)
5 | ENV['BUNDLE_GEMFILE'] = gemfile
6 | require 'bundler'
7 | Bundler.setup
8 | end
9 |
10 | $:.unshift File.expand_path('../../../../lib', __FILE__)
--------------------------------------------------------------------------------
/test/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 | require 'rake'
6 |
7 | Dummy::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= stylesheet_link_tag :all %>
6 | <%= javascript_include_tag :defaults %>
7 | <%= csrf_meta_tag %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/test/dummy/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/test/fixtures/tolk_phrases.yml:
--------------------------------------------------------------------------------
1 | hello_world:
2 | id: 1
3 | key: hello_world
4 |
5 | nested_hello_world:
6 | id: 2
7 | key: nested.hello_world
8 |
9 | nested_hello_country:
10 | id: 3
11 | key: nested.hello_country
12 |
13 | cozy:
14 | id: 4
15 | key: cozy
16 |
17 | human_format_precision:
18 | id: 5
19 | key: number.human.format.precision
--------------------------------------------------------------------------------
/lib/tolk.rb:
--------------------------------------------------------------------------------
1 | require 'will_paginate'
2 | require 'ya2yaml'
3 | require 'tolk/config'
4 | require 'tolk/engine'
5 | require 'tolk/sync'
6 | require 'tolk/import'
7 |
8 | module Tolk
9 | # Setup Tolk
10 | def self.config(&block)
11 | if block_given?
12 | block.call(Tolk::Config)
13 | else
14 | Tolk::Config
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/support/test_case.rb:
--------------------------------------------------------------------------------
1 | class ActiveSupport::TestCase
2 | Tolk::Config.reset
3 |
4 | self.use_transactional_fixtures = true
5 | self.use_instantiated_fixtures = false
6 |
7 | fixtures :all
8 |
9 | self.fixture_class_names = {:tolk_locales => 'Tolk::Locale', :tolk_phrases => 'Tolk::Phrase', :tolk_translations => 'Tolk::Translation'}
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/tolk/searches/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_tag tolk.search_path, :method => :get do %>
2 | <%= hidden_field_tag :locale, locale.name %>
3 | Search for
4 | <%= scope_selector_for(@locale) %>
5 | phrase:
6 | <%= text_field_tag :q, params[:q] %>
7 | within key:
8 | <%= text_field_tag :k, params[:k] %>
9 | <%= submit_tag "Search", :name => nil %>
10 | <% end %>
11 |
--------------------------------------------------------------------------------
/app/views/tolk/locales/show.atom.builder:
--------------------------------------------------------------------------------
1 | atom_feed do |feed|
2 | feed.title "Missing Translations for #{@locale.language_name} locale"
3 |
4 | @phrases.each do |phrase|
5 | feed.entry(phrase, :url => tolk_locale_url(@locale)) do |entry|
6 | entry.title(phrase.key)
7 | entry.content(phrase.key)
8 | entry.author {|author| author.name("Tolk") }
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/test/unit/config_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'fileutils'
3 |
4 | class ConfigTest < ActiveSupport::TestCase
5 | test "config default values" do
6 | assert_equal Proc, Tolk.config.dump_path.class
7 | assert_equal "#{Rails.root}/config/locales", Tolk::Locale._dump_path
8 | assert Tolk.config.mapping.keys.include?('ar')
9 | assert_equal 'Arabic',Tolk.config.mapping['ar']
10 | end
11 | end
--------------------------------------------------------------------------------
/lib/generators/tolk/templates/initializer.erb:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | # Tolk config file. Generated on <%= DateTime.now.to_s(:long) %>
4 | # See github.com/tolk/tolk for more informations
5 |
6 | Tolk.config do |config|
7 |
8 | # If you need to add a mapping do it like this :
9 | # May we suggest you use http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
10 | # config.mapping["fr-ES"] = 'Frañol !'
11 |
12 | end
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gem "rails", "3.2.6"
4 |
5 | gem 'will_paginate'
6 | gem "ya2yaml"
7 |
8 | group 'test' do
9 | gem "capybara", :git => "https://github.com/jnicklas/capybara.git"
10 | gem "factory_girl_rails"
11 | gem "sqlite3"
12 | gem "mocha"
13 | gem 'launchy'
14 | end
15 |
16 | group 'development' do
17 | if RUBY_VERSION < '1.9'
18 | gem "ruby-debug", ">= 0.10.3"
19 | end
20 | end
--------------------------------------------------------------------------------
/test/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 |
--------------------------------------------------------------------------------
/test/support/active_record_shared_connections.rb:
--------------------------------------------------------------------------------
1 | class ActiveRecord::Base
2 | mattr_accessor :shared_connection
3 | @@shared_connection = nil
4 |
5 | def self.connection
6 | @@shared_connection || retrieve_connection
7 | end
8 | end
9 |
10 | # Forces all threads to share the same connection. This works on
11 | # Capybara because it starts the web server in a thread.
12 | ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
13 |
--------------------------------------------------------------------------------
/app/controllers/tolk/searches_controller.rb:
--------------------------------------------------------------------------------
1 | module Tolk
2 | class SearchesController < Tolk::ApplicationController
3 | before_filter :find_locale
4 |
5 | def show
6 | @phrases = @locale.search_phrases(params[:q], params[:scope].to_sym, params[:k], params[:page])
7 | end
8 |
9 | private
10 |
11 | def find_locale
12 | @locale = Tolk::Locale.where('UPPER(name) = UPPER(?)', params[:locale]).first!
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Dummy::Application.config.session_store :cookie_store, :key => '_dummy_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rake db:sessions:create")
8 | # Dummy::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | Dummy::Application.config.secret_token = '15bf55619161ab494a0d716093754246f33c4ee07881afbf4d96b65d81075184aa798ce676d3a0b1b662ef8407e274e2a7b892721165cbac3ca72de3e9d882b1'
8 |
--------------------------------------------------------------------------------
/app/controllers/tolk/application_controller.rb:
--------------------------------------------------------------------------------
1 | require 'tolk/application_controller'
2 |
3 | module Tolk
4 | class ApplicationController < ActionController::Base
5 | helper :all
6 | protect_from_forgery
7 |
8 | cattr_accessor :authenticator
9 | before_filter :authenticate
10 |
11 | def authenticate
12 | self.authenticator.bind(self).call if self.authenticator && self.authenticator.respond_to?(:call)
13 | end
14 |
15 | def ensure_no_primary_locale
16 | redirect_to tolk.locales_path if @locale.primary?
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/public/dispatch.cgi:
--------------------------------------------------------------------------------
1 | #!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby
2 |
3 | require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
4 |
5 | # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
6 | # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
7 | require "dispatcher"
8 |
9 | ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
10 | Dispatcher.dispatch
11 |
--------------------------------------------------------------------------------
/public/dispatch.rb:
--------------------------------------------------------------------------------
1 | #!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby
2 |
3 | require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
4 |
5 | # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
6 | # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
7 | require "dispatcher"
8 |
9 | ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
10 | Dispatcher.dispatch
11 |
--------------------------------------------------------------------------------
/test/locales/formats/en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | number: 1
3 | string: I am just a stupid string :(
4 | variables: "String with variables {{hello}} and %{world}"
5 | variables_in_struct:
6 | none: "Without variable"
7 | one: "With {{variables}}"
8 | other: "With even %{more} {{variables}}"
9 | number_array: [1, 2, 3]
10 | string_array: ['sun', 'moon']
11 | pluralization:
12 | other: Hello
13 | not_pluralization:
14 | other: World
15 | lifo: fifo
16 | activerecord:
17 | models:
18 | person: Dude
19 | attributes:
20 | person:
21 | login: Handle
--------------------------------------------------------------------------------
/test/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3-ruby (not necessary on OS X Leopard)
3 | development:
4 | adapter: sqlite3
5 | database: db/development.sqlite3
6 | pool: 5
7 | timeout: 5000
8 |
9 | # Warning: The database defined as "test" will be erased and
10 | # re-generated from your development database when you run "rake".
11 | # Do not set this db to the same as development or production.
12 | test:
13 | adapter: sqlite3
14 | database: db/test.sqlite3
15 | pool: 5
16 | timeout: 5000
17 |
18 | production:
19 | adapter: sqlite3
20 | database: db/production.sqlite3
21 | pool: 5
22 | timeout: 5000
23 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/tolk/reset.css:
--------------------------------------------------------------------------------
1 | /*-------------------------------------------------
2 | RESET
3 | -------------------------------------------------*/
4 |
5 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td {
6 | margin:0;
7 | padding:0;
8 | }
9 | table {
10 | border-collapse:collapse;
11 | border-spacing:0;
12 | }
13 | fieldset,img {border:0;}
14 | address,caption,cite,code,dfn,th,var {
15 | font-style:normal;
16 | font-weight:normal;
17 | }
18 |
19 | h1,h2,h3,h4,h5,h6 {
20 | font-size:100%;
21 | font-weight:normal;
22 | }
23 |
24 | ol,ul {list-style:none;}
25 | caption,th {text-align:left;}
26 | q:before,q:after {content:'';}
27 | abbr,acronym {border:0;}
28 |
29 | img {border: none;}
30 | em em {font-style: normal;}
--------------------------------------------------------------------------------
/app/models/tolk/phrase.rb:
--------------------------------------------------------------------------------
1 | module Tolk
2 | class Phrase < ActiveRecord::Base
3 | self.table_name = "tolk_phrases"
4 |
5 | attr_accessible :key
6 |
7 | validates_uniqueness_of :key
8 |
9 | cattr_accessor :per_page
10 | self.per_page = 30
11 |
12 | has_many :translations, :class_name => 'Tolk::Translation', :dependent => :destroy do
13 | def primary
14 | to_a.detect {|t| t.locale_id == Tolk::Locale.primary_locale.id}
15 | end
16 |
17 | def for(locale)
18 | to_a.detect {|t| t.locale_id == locale.id}
19 | end
20 | end
21 |
22 | attr_accessor :translation
23 |
24 | scope :containing_text, lambda { |query|
25 | { :conditions => ["tolk_phrases.key LIKE ?", "%#{query}%"] }
26 | }
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/test/unit/import_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'fileutils'
3 |
4 | class ImportTest < ActiveSupport::TestCase
5 | def setup
6 | Tolk::Locale.delete_all
7 | Tolk::Translation.delete_all
8 | Tolk::Phrase.delete_all
9 |
10 | Tolk::Locale.locales_config_path = Rails.root.join("../locales/import/")
11 |
12 | I18n.backend.reload!
13 | I18n.load_path = [Tolk::Locale.locales_config_path + 'en.yml']
14 | I18n.backend.send :init_translations
15 |
16 | Tolk::Locale.primary_locale(true)
17 | end
18 |
19 | test "skips gem translations files (xxx.en.yml)" do
20 | Tolk::Locale.sync!
21 | Tolk::Locale.import_secondary_locales
22 |
23 |
24 | assert_equal 0, Tolk::Phrase.where(key: 'gem_hello_world').count
25 |
26 | end
27 |
28 | end
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | * Tolk 1.3.2
2 | * fixed a crash and made tests passing (gshilin)
3 |
4 | * Tolk 1.3.0
5 | * Improved header (bquorning)
6 | * Locales are ordered by name in locales/index
7 | * Tolk ignore files with name of type xxx.en.yml
8 | * Default mapping use http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
9 | * Adding a config file for Tolk in the hosted application
10 | * Allow to config Mappings
11 | * Allow to config dump_path
12 |
13 | * Tolk 1.2.0 [May 16th, 2012]
14 | * Adding search within key (ZenCocoon)
15 | * Update for Rails 3.2.3 whitelist attributes compatibility
16 |
17 |
18 | * Tolk 1.1.0 [May 15th, 2012]
19 | * Update for Rails 3.2 compatibility
20 | * Added Travis CI
21 |
22 | * Tolk 1.0.0 [June 6th, 2010]
23 | * First public release as a Rails 3 engine gem
--------------------------------------------------------------------------------
/test/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The change you wanted was rejected.
23 |
Maybe you tried to change something you didn't have access to.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/test/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The page you were looking for doesn't exist.
23 |
You may have mistyped the address or the page may have moved.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/test/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
We're sorry, but something went wrong.
23 |
We've been notified about this issue and we'll take a look at it shortly.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/test/fixtures/tolk_translations.yml:
--------------------------------------------------------------------------------
1 | hello_world_en:
2 | id: 1
3 | phrase_id: 1
4 | locale_id: 1
5 | text: Hello World
6 |
7 | hello_world_da:
8 | id: 2
9 | phrase_id: 1
10 | locale_id: 2
11 | text: Hej Verden
12 |
13 | hello_world_se:
14 | id: 3
15 | phrase_id: 1
16 | locale_id: 3
17 | text: Hejsan Verdon
18 |
19 | nested_hello_world_en:
20 | id: 4
21 | phrase_id: 2
22 | locale_id: 1
23 | text: Nested Hello World
24 |
25 | nested_hello_country_en:
26 | id: 5
27 | phrase_id: 3
28 | locale_id: 1
29 | text: Nested Hello Country
30 |
31 | nested_hello_world_da:
32 | id: 6
33 | phrase_id: 2
34 | locale_id: 2
35 | text: Nedarvet Hej Verden
36 |
37 | cozy_da:
38 | id: 7
39 | phrase_id: 4
40 | locale_id: 2
41 | text: Hyggeligt
42 |
43 | human_format_precision_en:
44 | id: 8
45 | phrase_id: 5
46 | locale_id: 1
47 | text: "1"
48 |
--------------------------------------------------------------------------------
/app/views/layouts/tolk/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Tolk
7 |
8 | <%= javascript_include_tag "tolk/application" %>
9 | <%= stylesheet_link_tag "tolk/application" %>
10 | <%= yield :head %>
11 |
12 |
13 |
14 |
15 |
16 |
<%= link_to "Tolk", root_path %><% if @locale.present? %><%= link_to @locale.language_name, locale_path(@locale) %><% end %>
17 |
18 | <%= yield %>
19 |
20 |
21 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/integration/authentication_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class AuthenticationTest < ActionController::IntegrationTest
4 | self.fixture_path = Rails.root.to_s + "/../fixtures"
5 | fixtures :all
6 |
7 | def setup
8 | Tolk::ApplicationController.authenticator = proc do
9 | authenticate_or_request_with_http_basic {|user_name, password| user_name == 'lifo' && password == 'pass' }
10 | end
11 | end
12 |
13 | def teardown
14 | Tolk::ApplicationController.authenticator = nil
15 | end
16 |
17 | test "failed authentication" do
18 | get '/tolk'
19 | assert_response 401
20 | end
21 |
22 | test "successful authentication" do
23 | get '/tolk', nil, 'HTTP_AUTHORIZATION' => encode_credentials('lifo', 'pass')
24 | assert_response :success
25 | end
26 |
27 | protected
28 |
29 | def encode_credentials(username, password)
30 | "Basic #{Base64.encode64("#{username}:#{password}")}"
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/tolk.gemspec:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | require File.expand_path('../lib/tolk/version', __FILE__)
3 |
4 | Gem::Specification.new do |s|
5 | s.name = 'tolk'
6 | s.version = Tolk::VERSION
7 | s.summary = 'Rails engine providing web interface for managing i18n yaml files'
8 | s.description = 'Tolk is a web interface for doing i18n translations packaged as an engine for Rails applications.'
9 |
10 | s.authors = ['David Heinemeier Hansson', 'Piotr Sarnacki', 'Emilio Tagua', 'Thomas Darde']
11 | s.email = 'david@loudthinking.com'
12 | s.homepage = 'http://github.com/tolk/tolk'
13 |
14 | s.platform = Gem::Platform::RUBY
15 | s.add_dependency('will_paginate')
16 | s.add_dependency('ya2yaml', '~> 0.26')
17 |
18 | if File.exists?('UPGRADING')
19 | s.post_install_message = File.read("UPGRADING")
20 | end
21 |
22 | s.files = Dir['README', 'MIT-LICENSE', 'config/routes.rb', 'init.rb', 'lib/**/*', 'app/**/*', 'public/tolk/**/*']
23 |
24 | s.require_path = 'lib'
25 | end
26 |
--------------------------------------------------------------------------------
/app/views/tolk/locales/index.html.erb:
--------------------------------------------------------------------------------
1 | Locales Primary locale is <%= Tolk::Locale.primary_locale.language_name %>
2 | <% if @locales.any? %>
3 |
4 | <% @locales.each do |locale| %>
5 | -
6 | <%= link_to locale.language_name, locale %>
7 | <% missing_count = locale.count_phrases_without_translation %>
8 | <% if missing_count > 0 %>
9 | <%= locale.count_phrases_without_translation %>
10 | <% end %>
11 |
12 | <% end %>
13 |
14 |
15 | <% else %>
16 | No locales yet.
17 |
18 | <% end %>
19 | <%= form_for(Tolk::Locale.new) do |f| %>
20 |
21 |
22 | <%= f.label "Add a new Locale" %>
23 |
26 | <%= f.submit 'Add' %>
27 |
28 |
29 | <% end %>
30 |
31 |
32 |
--------------------------------------------------------------------------------
/public/dispatch.fcgi:
--------------------------------------------------------------------------------
1 | #!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby
2 | #
3 | # You may specify the path to the FastCGI crash log (a log of unhandled
4 | # exceptions which forced the FastCGI instance to exit, great for debugging)
5 | # and the number of requests to process before running garbage collection.
6 | #
7 | # By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
8 | # and the GC period is nil (turned off). A reasonable number of requests
9 | # could range from 10-100 depending on the memory footprint of your app.
10 | #
11 | # Example:
12 | # # Default log path, normal GC behavior.
13 | # RailsFCGIHandler.process!
14 | #
15 | # # Default log path, 50 requests between GC.
16 | # RailsFCGIHandler.process! nil, 50
17 | #
18 | # # Custom log path, normal GC behavior.
19 | # RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
20 | #
21 | require File.dirname(__FILE__) + "/../config/environment"
22 | require 'fcgi_handler'
23 |
24 | RailsFCGIHandler.process!
25 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 | The change you wanted was rejected (422)
9 |
21 |
22 |
23 |
24 |
25 |
26 |
The change you wanted was rejected.
27 |
Maybe you tried to change something you didn't have access to.
28 |
29 |
30 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 | The page you were looking for doesn't exist (404)
9 |
21 |
22 |
23 |
24 |
25 |
26 |
The page you were looking for doesn't exist.
27 |
You may have mistyped the address or the page may have moved.
28 |
29 |
30 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/environment.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the webserver when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger
20 | config.active_support.deprecation = :log
21 |
22 | # Only use best-standards-support built into browsers
23 | config.action_dispatch.best_standards_support = :builtin
24 | end
25 |
26 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2004-2010 David Heinemeier Hansson
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.
--------------------------------------------------------------------------------
/app/helpers/tolk/application_helper.rb:
--------------------------------------------------------------------------------
1 | module Tolk
2 | module ApplicationHelper
3 | def format_i18n_value(value)
4 | h(yaml_value(value)).gsub(/\n/, '↵
').html_safe
5 | end
6 |
7 | def format_i18n_text_area_value(value)
8 | yaml_value(value)
9 | end
10 |
11 | def yaml_value(value)
12 | if value.present?
13 | unless value.is_a?(String)
14 | value = value.respond_to?(:ya2yaml) ? value.ya2yaml(:syck_compatible => true) : value.to_yaml
15 | end
16 | end
17 |
18 | value
19 | end
20 |
21 | def tolk_locale_selection
22 | existing_locale_names = Tolk::Locale.all.map(&:name)
23 |
24 | pairs = Tolk.config.mapping.to_a.map(&:reverse).sort_by(&:first)
25 | pairs.reject {|pair| existing_locale_names.include?(pair.last) }
26 | end
27 |
28 | def scope_selector_for(locale)
29 | select_tag 'scope', options_for_select([[Tolk::Locale.primary_locale.language_name, "origin"],
30 | [locale.language_name, "target"]], params[:scope])
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/generators/tolk/templates/migration.rb:
--------------------------------------------------------------------------------
1 | class CreateTolkTables < ActiveRecord::Migration
2 | def self.up
3 | create_table :tolk_locales do |t|
4 | t.string :name
5 | t.datetime :created_at
6 | t.datetime :updated_at
7 | end
8 |
9 | add_index :tolk_locales, :name, :unique => true
10 |
11 | create_table :tolk_phrases do |t|
12 | t.text :key
13 | t.datetime :created_at
14 | t.datetime :updated_at
15 | end
16 |
17 | create_table :tolk_translations do |t|
18 | t.integer :phrase_id
19 | t.integer :locale_id
20 | t.text :text
21 | t.text :previous_text
22 | t.boolean :primary_updated, :default => false
23 | t.datetime :created_at
24 | t.datetime :updated_at
25 | end
26 |
27 | add_index :tolk_translations, [:phrase_id, :locale_id], :unique => true
28 | end
29 |
30 | def self.down
31 | remove_index :tolk_translations, :column => [:phrase_id, :locale_id]
32 | remove_index :tolk_locales, :column => :name
33 |
34 | drop_table :tolk_translations
35 | drop_table :tolk_phrases
36 | drop_table :tolk_locales
37 | end
38 | end
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | require 'rake'
3 | require 'rdoc/task'
4 | require 'rubygems/package_task'
5 |
6 | require 'rake/testtask'
7 |
8 | Rake::TestTask.new(:test) do |t|
9 | t.libs << 'lib'
10 | t.libs << 'test'
11 | t.pattern = 'test/**/*_test.rb'
12 | t.verbose = false
13 | end
14 |
15 | task :default => :test
16 |
17 | Rake::RDocTask.new(:rdoc) do |rdoc|
18 | rdoc.rdoc_dir = 'rdoc'
19 | rdoc.title = 'Tolk'
20 | rdoc.options << '--line-numbers' << '--inline-source'
21 | rdoc.rdoc_files.include('README.rdoc')
22 | rdoc.rdoc_files.include('lib/**/*.rb')
23 | end
24 |
25 | spec = Gem::Specification.new do |s|
26 | s.name = "tolk"
27 | s.summary = "Rails engine providing web interface for managing i18n yaml files"
28 | s.description = "Tolk is a web interface for doing i18n translations packaged as an engine for Rails applications."
29 | s.files = FileList["[A-Z]*", "lib/**/*"]
30 | s.version = "2.0.0.beta"
31 | end
32 |
33 | Gem::PackageTask.new(spec) do |pkg|
34 | end
35 |
36 | desc "Install the gem #{spec.name}-#{spec.version}.gem"
37 | task :install do
38 | system("gem install pkg/#{spec.name}-#{spec.version}.gem --no-ri --no-rdoc")
39 | end
40 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 | We're sorry, but something went wrong (500)
9 |
21 |
22 |
23 |
24 |
25 |
26 |
We're sorry, but something went wrong.
27 |
We've been notified about this issue and we'll take a look at it shortly.
28 |
(If you're the administrator of this website, then please read
29 | the log file "<%=h RAILS_ENV %>.log"
30 | to find out what went wrong.)
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/lib/generators/tolk/utils.rb:
--------------------------------------------------------------------------------
1 | module Tolk
2 | module Generators
3 | module Utils
4 | module InstanceMethods
5 | def display(output, color = :green)
6 | say(" - #{output}", color)
7 | end
8 |
9 | def ask_for(wording, default_value = nil, override_if_present_value = nil)
10 | override_if_present_value.present? ?
11 | display("Using [#{override_if_present_value}] for question '#{wording}'") && override_if_present_value :
12 | ask(" ? #{wording} Press for [#{default_value}] >", :yellow).presence || default_value
13 | end
14 |
15 | def ask_boolean(wording, default_value = nil)
16 | value = ask_for(wording, 'Y')
17 | value = (value == 'Y')
18 | end
19 | end
20 |
21 | module ClassMethods
22 | def next_migration_number(dirname)
23 | if ActiveRecord::Base.timestamped_migrations
24 | migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
25 | migration_number += 1
26 | migration_number.to_s
27 | else
28 | "%.3d" % (current_migration_number(dirname) + 1)
29 | end
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
36 |
--------------------------------------------------------------------------------
/db/migrate/20110824135256_migrate_tolk.rb:
--------------------------------------------------------------------------------
1 | class MigrateTolk < ActiveRecord::Migration
2 | def self.up
3 | create_table "tolk_locales", :force => true do |t|
4 | t.string "name"
5 | t.datetime "created_at"
6 | t.datetime "updated_at"
7 | end
8 |
9 | add_index "tolk_locales", ["name"], :name => "index_tolk_locales_on_name", :unique => true
10 |
11 | create_table "tolk_phrases", :force => true do |t|
12 | t.text "key"
13 | t.datetime "created_at"
14 | t.datetime "updated_at"
15 | end
16 |
17 | add_index "tolk_phrases", ["key"], :name => "index_tolk_phrases_on_key", :unique => true
18 |
19 | create_table "tolk_translations", :force => true do |t|
20 | t.integer "phrase_id"
21 | t.integer "locale_id"
22 | t.text "text"
23 | t.datetime "created_at"
24 | t.datetime "updated_at"
25 | t.boolean "primary_updated", :default => false
26 | t.text "previous_text"
27 | end
28 |
29 | add_index "tolk_translations", ["phrase_id", "locale_id"], :name => "index_tolk_translations_on_phrase_id_and_locale_id", :unique => true
30 | end
31 |
32 | def self.down
33 | drop_table :tolk_translations
34 | drop_table :tolk_phrases
35 | drop_table :tolk_locales
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Configure Rails Envinronment
2 | ENV["RAILS_ENV"] = "test"
3 |
4 | require File.expand_path("../dummy/config/environment.rb", __FILE__)
5 | ActiveRecord::IdentityMap.enabled = false
6 | require "rails/test_help"
7 |
8 | ActiveSupport::TestCase.fixture_path = Rails.root.to_s + "/../fixtures"
9 | ActiveSupport::TestCase.use_transactional_fixtures = true
10 |
11 | ActionMailer::Base.delivery_method = :test
12 | ActionMailer::Base.perform_deliveries = true
13 | ActionMailer::Base.default_url_options[:host] = "test.com"
14 |
15 | Rails.backtrace_cleaner.remove_silencers!
16 |
17 | # Configure capybara for integration testing
18 | require "capybara/rails"
19 | Capybara.default_driver = :selenium
20 | Capybara.default_selector = :css
21 |
22 | # Run any available migrations
23 | # A bit of hacks, find a nicer way
24 | FileUtils.rm(Dir[File.expand_path("../dummy/db/test.sqlite3", __FILE__)])
25 | FileUtils.rm(Dir[File.expand_path("../dummy/db/migrate/*.tolk.rb", __FILE__)])
26 | ActiveRecord::Migration.copy File.expand_path("../dummy/db/migrate/", __FILE__), { :tolk => File.expand_path("../../db/migrate/", __FILE__) }
27 | ActiveRecord::Migrator.migrate File.expand_path("../dummy/db/migrate/", __FILE__)
28 |
29 | # Load support files
30 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
31 |
--------------------------------------------------------------------------------
/lib/tasks/tolk_tasks.rake:
--------------------------------------------------------------------------------
1 | namespace :tolk do
2 | desc "Update locale"
3 | task :update_locale, [:old_name, :new_name] => :environment do |t, args|
4 | old_name, new_name = args[:old_name], args[:new_name]
5 | puts Tolk::Locale.rename(old_name, new_name)
6 | end
7 |
8 | desc "Add database tables, copy over the assets, and import existing translations"
9 | task :setup => :environment do
10 | system 'rails g tolk:install'
11 |
12 | Rake::Task['db:migrate'].invoke
13 | Rake::Task['tolk:sync'].invoke
14 | Rake::Task['tolk:import'].invoke
15 | end
16 |
17 | desc "Sync Tolk with the default locale's yml file"
18 | task :sync => :environment do
19 | Tolk::Locale.sync!
20 | end
21 |
22 | desc "Generate yml files for all the locales defined in Tolk"
23 | task :dump_all => :environment do
24 | Tolk::Locale.dump_all
25 | end
26 |
27 | desc "Imports data all non default locale yml files to Tolk"
28 | task :import => :environment do
29 | Rake::Task['tolk:sync'].invoke
30 | Tolk::Locale.import_secondary_locales
31 | end
32 |
33 | desc "Show all the keys potentially containing HTML values and no _html postfix"
34 | task :html_keys => :environment do
35 | bad_translations = Tolk::Locale.primary_locale.translations_with_html
36 | bad_translations.each do |bt|
37 | puts "#{bt.phrase.key} - #{bt.text}"
38 | end
39 | end
40 |
41 | end
42 |
--------------------------------------------------------------------------------
/app/controllers/tolk/locales_controller.rb:
--------------------------------------------------------------------------------
1 | module Tolk
2 | class LocalesController < Tolk::ApplicationController
3 | before_filter :find_locale, :only => [:show, :all, :update, :updated]
4 | before_filter :ensure_no_primary_locale, :only => [:all, :update, :show, :updated]
5 |
6 | def index
7 | @locales = Tolk::Locale.secondary_locales.sort_by(&:language_name)
8 | end
9 |
10 | def show
11 | respond_to do |format|
12 | format.html do
13 | @phrases = @locale.phrases_without_translation(params[:page])
14 | end
15 | format.atom { @phrases = @locale.phrases_without_translation(params[:page], :per_page => 50) }
16 | format.yaml { render :text => @locale.to_hash.ya2yaml(:syck_compatible => true) }
17 | end
18 | end
19 |
20 | def update
21 | @locale.translations_attributes = params[:translations]
22 | @locale.save
23 | redirect_to request.referrer
24 | end
25 |
26 | def all
27 | @phrases = @locale.phrases_with_translation(params[:page])
28 | end
29 |
30 | def updated
31 | @phrases = @locale.phrases_with_updated_translation(params[:page])
32 | render :all
33 | end
34 |
35 | def create
36 | Tolk::Locale.create!(params[:tolk_locale])
37 | redirect_to :action => :index
38 | end
39 |
40 | private
41 |
42 | def find_locale
43 | @locale = Tolk::Locale.where('UPPER(name) = UPPER(?)', params[:id]).first!
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/test/unit/translation_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'fileutils'
3 |
4 | class TranslationTest < ActiveSupport::TestCase
5 | fixtures :tolk_translations
6 |
7 | def setup
8 | Tolk::Locale.primary_locale(true)
9 | end
10 |
11 | test "translation is inavlid when a duplicate exists" do
12 | translation = Tolk::Translation.new :phrase => tolk_translations(:hello_world_da).phrase, :locale => tolk_translations(:hello_world_da).locale
13 | translation.text = "Revised Hello World"
14 | assert translation.invalid?
15 | assert translation.errors[:phrase_id]
16 | end
17 |
18 | test "translation is not changed when text is assigned an equal value in numberic form" do
19 | translation = tolk_translations(:human_format_precision_en)
20 | assert_equal "1", translation.text
21 | translation.text = "1"
22 | assert_equal false, translation.changed?
23 | translation.text = 1
24 | assert_equal false, translation.changed?
25 | end
26 |
27 | test "translation with string value" do
28 | assert_equal "Hello World", tolk_translations(:hello_world_en).value
29 | end
30 |
31 | test "translation with string value with variables" do
32 | text = "{{attribute}} {{message}}"
33 | assert_equal text, Tolk::Translation.new(:text => text).value
34 | end
35 |
36 | test "translation with numeric value" do
37 | assert_equal 1, tolk_translations(:human_format_precision_en).value
38 | end
39 |
40 | test "translation with hash value" do
41 | hash = {:foo => "bar"}
42 | assert_equal hash, Tolk::Translation.new(:text => hash).value
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/environment.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Log error messages when you accidentally call methods on nil.
11 | config.whiny_nils = true
12 |
13 | # Show full error reports and disable caching
14 | config.consider_all_requests_local = true
15 | config.action_controller.perform_caching = false
16 |
17 | # Raise exceptions instead of rendering exception templates
18 | config.action_dispatch.show_exceptions = false
19 |
20 | # Disable request forgery protection in test environment
21 | config.action_controller.allow_forgery_protection = false
22 |
23 | # Tell Action Mailer not to deliver emails to the real world.
24 | # The :test delivery method accumulates sent emails in the
25 | # ActionMailer::Base.deliveries array.
26 | config.action_mailer.delivery_method = :test
27 |
28 | # Use SQL instead of Active Record's schema dumper when creating the test database.
29 | # This is necessary if your schema can't be completely dumped by the schema dumper,
30 | # like if you have constraints or database-specific column types
31 | # config.active_record.schema_format = :sql
32 |
33 | # Print deprecation notices to the stderr
34 | config.active_support.deprecation = :stderr
35 | end
36 |
--------------------------------------------------------------------------------
/test/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended to check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(:version => 20100910193132) do
14 |
15 | create_table "tolk_locales", :force => true do |t|
16 | t.string "name"
17 | t.datetime "created_at"
18 | t.datetime "updated_at"
19 | end
20 |
21 | add_index "tolk_locales", ["name"], :name => "index_tolk_locales_on_name", :unique => true
22 |
23 | create_table "tolk_phrases", :force => true do |t|
24 | t.text "key"
25 | t.datetime "created_at"
26 | t.datetime "updated_at"
27 | end
28 |
29 | create_table "tolk_translations", :force => true do |t|
30 | t.integer "phrase_id"
31 | t.integer "locale_id"
32 | t.text "text"
33 | t.datetime "created_at"
34 | t.datetime "updated_at"
35 | t.boolean "primary_updated", :default => false
36 | t.text "previous_text"
37 | end
38 |
39 | add_index "tolk_translations", ["phrase_id", "locale_id"], :name => "index_tolk_translations_on_phrase_id_and_locale_id", :unique => true
40 |
41 | end
42 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead of editing this file,
2 | # please use the migrations feature of Active Record to incrementally modify your database, and
3 | # then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your database schema. If you need
6 | # to create the application database on another system, you should be using db:schema:load, not running
7 | # all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
8 | # you'll amass, the slower it'll run and the greater likelihood for issues).
9 | #
10 | # It's strongly recommended to check this file into your version control system.
11 |
12 | ActiveRecord::Schema.define(:version => 20100409194926) do
13 |
14 | create_table "tolk_locales", :force => true do |t|
15 | t.string "name"
16 | t.datetime "created_at"
17 | t.datetime "updated_at"
18 | end
19 |
20 | add_index "tolk_locales", ["name"], :name => "index_tolk_locales_on_name", :unique => true
21 |
22 | create_table "tolk_phrases", :force => true do |t|
23 | t.text "key"
24 | t.datetime "created_at"
25 | t.datetime "updated_at"
26 | end
27 |
28 | add_index "tolk_phrases", ["key"], :name => "index_tolk_phrases_on_key", :unique => true
29 |
30 | create_table "tolk_translations", :force => true do |t|
31 | t.integer "phrase_id"
32 | t.integer "locale_id"
33 | t.text "text"
34 | t.datetime "created_at"
35 | t.datetime "updated_at"
36 | t.boolean "primary_updated", :default => false
37 | t.text "previous_text"
38 | end
39 |
40 | add_index "tolk_translations", ["phrase_id", "locale_id"], :name => "index_tolk_translations_on_phrase_id_and_locale_id", :unique => true
41 |
42 | end
43 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/environment.rb
3 |
4 | # The production environment is meant for finished, "live" apps.
5 | # Code is not reloaded between requests
6 | config.cache_classes = true
7 |
8 | # Full error reports are disabled and caching is turned on
9 | config.consider_all_requests_local = false
10 | config.action_controller.perform_caching = true
11 |
12 | # Specifies the header that your server uses for sending files
13 | config.action_dispatch.x_sendfile_header = "X-Sendfile"
14 |
15 | # For nginx:
16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
17 |
18 | # If you have no front-end server that supports something like X-Sendfile,
19 | # just comment this out and Rails will serve the files
20 |
21 | # See everything in the log (default is :info)
22 | # config.log_level = :debug
23 |
24 | # Use a different logger for distributed setups
25 | # config.logger = SyslogLogger.new
26 |
27 | # Use a different cache store in production
28 | # config.cache_store = :mem_cache_store
29 |
30 | # Disable Rails's static asset server
31 | # In production, Apache or nginx will already do this
32 | config.serve_static_assets = false
33 |
34 | # Enable serving of images, stylesheets, and javascripts from an asset server
35 | # config.action_controller.asset_host = "http://assets.example.com"
36 |
37 | # Disable delivery errors, bad email addresses will be ignored
38 | # config.action_mailer.raise_delivery_errors = false
39 |
40 | # Enable threaded mode
41 | # config.threadsafe!
42 |
43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
44 | # the I18n.default_locale when a translation can not be found)
45 | config.i18n.fallbacks = true
46 |
47 | # Send deprecation notices to registered listeners
48 | config.active_support.deprecation = :notify
49 | end
50 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require "active_model/railtie"
4 | require "active_record/railtie"
5 | require "action_controller/railtie"
6 | require "action_view/railtie"
7 | require "action_mailer/railtie"
8 |
9 | Bundler.require
10 | require "tolk"
11 |
12 | module Dummy
13 | class Application < Rails::Application
14 | # Settings in config/environments/* take precedence over those specified here.
15 | # Application configuration should go into files in config/initializers
16 | # -- all .rb files in that directory are automatically loaded.
17 |
18 | # Custom directories with classes and modules you want to be autoloadable.
19 | # config.autoload_paths += %W(#{config.root}/extras)
20 |
21 | # Only load the plugins named here, in the order given (default is alphabetical).
22 | # :all can be used as a placeholder for all plugins not explicitly named.
23 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
24 |
25 | # Activate observers that should always be running.
26 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
27 |
28 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
29 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
30 | # config.time_zone = 'Central Time (US & Canada)'
31 |
32 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
33 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
34 | # config.i18n.default_locale = :de
35 |
36 | # JavaScript files you want as :defaults (application.js is always included).
37 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
38 |
39 | # Configure the default encoding used in templates for Ruby 1.9.
40 | config.encoding = "utf-8"
41 |
42 | # Configure sensitive parameters which will be filtered from the log file.
43 | config.filter_parameters += [:password]
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/tolk/import.rb:
--------------------------------------------------------------------------------
1 | module Tolk
2 | module Import
3 | def self.included(base)
4 | base.send :extend, ClassMethods
5 | end
6 |
7 | module ClassMethods
8 |
9 | def import_secondary_locales
10 | locales = Dir.entries(self.locales_config_path)
11 |
12 | locale_block_filter = Proc.new {
13 | |l| ['.', '..'].include?(l) ||
14 | !l.ends_with?('.yml') ||
15 | l.match(/(.*\.){2,}/) # reject files of type xxx.en.yml
16 | }
17 | locales = locales.reject(&locale_block_filter).map {|x| x.split('.').first }
18 | locales = locales - [Tolk::Locale.primary_locale.name]
19 | locales.each {|l| import_locale(l) }
20 | end
21 |
22 | def import_locale(locale_name)
23 | locale = Tolk::Locale.find_or_create_by_name(locale_name)
24 | data = locale.read_locale_file
25 | return unless data
26 |
27 | phrases = Tolk::Phrase.all
28 | count = 0
29 |
30 | data.each do |key, value|
31 | phrase = phrases.detect {|p| p.key == key}
32 |
33 | if phrase
34 | translation = locale.translations.new(:text => value, :phrase => phrase)
35 | count = count + 1 if translation.save
36 | else
37 | puts "[ERROR] Key '#{key}' was found in #{locale_name}.yml but #{Tolk::Locale.primary_language_name} translation is missing"
38 | end
39 | end
40 |
41 | puts "[INFO] Imported #{count} keys from #{locale_name}.yml"
42 | end
43 |
44 | end
45 |
46 | def read_locale_file
47 | locale_file = "#{self.locales_config_path}/#{self.name}.yml"
48 | raise "Locale file #{locale_file} does not exists" unless File.exists?(locale_file)
49 |
50 | puts "[INFO] Reading #{locale_file} for locale #{self.name}"
51 | begin
52 | self.class.flat_hash(YAML::load(IO.read(locale_file))[self.name])
53 | rescue
54 | puts "[ERROR] File #{locale_file} expected to declare #{self.name} locale, but it does not. Skipping this file."
55 | nil
56 | end
57 |
58 | end
59 |
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/generators/tolk/install_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails/generators'
2 | require File.expand_path('../utils', __FILE__)
3 |
4 | # http://guides.rubyonrails.org/generators.html
5 | # http://rdoc.info/github/wycats/thor/master/Thor/Actions.html
6 | # keep generator idempotent, thanks
7 | # Thanks to https://github.com/sferik/rails_admin !
8 |
9 | module Tolk
10 | class InstallGenerator < Rails::Generators::Base
11 |
12 | source_root File.expand_path("../templates", __FILE__)
13 | include Rails::Generators::Migration
14 | include Generators::Utils::InstanceMethods
15 | extend Generators::Utils::ClassMethods
16 |
17 | argument :_namespace, :type => :string, :required => false, :desc => "Tolk url namespace"
18 | desc "Tolk installation generator"
19 | def install
20 | routes = File.open(Rails.root.join("config/routes.rb")).try :read
21 | initializer = (File.open(Rails.root.join("config/initializers/tolk.rb")) rescue nil).try :read
22 |
23 | display "Hello, Tolk installer will help you sets things up!", :black
24 | unless initializer
25 | install_generator = ask_boolean("Do you wan't to install the optional configuration file (to change mappings, locales dump location etc..) ?")
26 | template "initializer.erb", "config/initializers/tolk.rb" if install_generator
27 | else
28 | display "You already have a config file. You're updating, heh? I'm generating a new 'tolk.rb.example' that you can review."
29 | template "initializer.erb", "config/initializers/tolk.rb.example"
30 | end
31 |
32 | display "Adding a migration..."
33 | migration_template 'migration.rb', 'db/migrate/create_tolk_tables.rb' rescue display $!.message
34 |
35 | namespace = ask_for("Where do you want to mount tolk?", "tolk", _namespace)
36 | gsub_file "config/routes.rb", /mount Tolk::Engine => \'\/.+\', :as => \'tolk\'/, ''
37 | gsub_file "config/routes.rb", /mount Tolk::Engine => \'\/.+\'/, ''
38 | route("mount Tolk::Engine => '/#{namespace}', :as => 'tolk'")
39 |
40 | display "Job's done: migrate, start your server and visit '/#{namespace}'!", :blue
41 |
42 | end
43 | end
44 | end
--------------------------------------------------------------------------------
/lib/tolk/config.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/class/attribute_accessors'
2 |
3 | module Tolk
4 | module Config
5 |
6 |
7 | class << self
8 | # Mapping : a hash of the type { 'ar' => 'Arabic' }
9 | attr_accessor :mapping
10 |
11 | # Dump locale path by default the locales folder (config/locales)
12 | attr_accessor :dump_path
13 |
14 | def reset
15 | @dump_path = Proc.new { "#{Rails.application.root}/config/locales" }
16 |
17 | @mapping = {
18 | 'ar' => 'Arabic',
19 | 'bs' => 'Bosnian',
20 | 'bg' => 'Bulgarian',
21 | 'ca' => 'Catalan',
22 | 'cs' => 'Czech',
23 | 'da' => 'Danish',
24 | 'de' => 'German',
25 | 'el' => 'Greek',
26 | 'en' => 'English',
27 | 'es' => 'Spanish',
28 | 'et' => 'Estonian',
29 | 'fa' => 'Persian',
30 | 'fi' => 'Finnish',
31 | 'fr' => 'French',
32 | 'he' => 'Hebrew',
33 | 'hr' => 'Croatian',
34 | 'hu' => 'Hungarian',
35 | 'id' => 'Indonesian',
36 | 'is' => 'Icelandic',
37 | 'it' => 'Italian',
38 | 'ja' => 'Japanese',
39 | 'ko' => 'Korean',
40 | 'lo' => 'Lao',
41 | 'lt' => 'Lithuanian',
42 | 'lv' => 'Latvian',
43 | 'mk' => 'Macedonian',
44 | 'nl' => 'Dutch',
45 | 'no' => 'Norwegian',
46 | 'pl' => 'Polish',
47 | 'pt-br' => 'Portuguese (Brazilian)',
48 | 'pt-PT' => 'Portuguese (Portugal)',
49 | 'ro' => 'Romanian',
50 | 'ru' => 'Russian',
51 | 'sv' => 'Swedish',
52 | 'sk' => 'Slovak',
53 | 'sl' => 'Slovene',
54 | 'sr' => 'Serbian',
55 | 'sw' => 'Swahili',
56 | 'th' => 'Thai',
57 | 'tr' => 'Turkish',
58 | 'uk' => 'Ukrainian',
59 | 'vi' => 'Vietnamese',
60 | 'zh-CN' => 'Chinese (Simplified)',
61 | 'zh-TW' => 'Chinese (Traditional)'
62 | }
63 | end
64 | end
65 |
66 | # Set default values for configuration options on load
67 | self.reset
68 | end
69 | end
--------------------------------------------------------------------------------
/app/views/tolk/searches/show.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= render :partial => "form", :locals => { :locale => @locale } %>
3 |
4 |
5 |
6 | <%= link_to "Phrases missing translation", @locale %>
7 |
8 | <%= link_to "Completed translations", tolk.all_locale_path(@locale) %>
9 |
10 |
11 |
12 | <% if @phrases.any? %>
13 | <%= form_for @locale do |locale_form| %>
14 |
15 |
16 | | <%= @locale.language_name -%> |
17 | <%= Tolk::Locale.primary_language_name -%> |
18 |
19 | <% @phrases.each do |phrase| %>
20 |
21 | <% if translation = phrase.translations.find_by_locale_id(@locale.id) || Tolk::Translation.new(:locale => @locale, :phrase => phrase) %>
22 |
23 | |
24 | <%= hidden_field_tag :"translations[][id]", translation.id, :id => "#{translation.object_id}_id" %>
25 | <%= hidden_field_tag :"translations[][phrase_id]", phrase.id, :id => "#{translation.object_id}_phrase_id" %>
26 | <%= hidden_field_tag :"translations[][locale_id]", translation.locale_id, :id => "#{translation.object_id}_locale_id" %>
27 | <%= text_area_tag :"translations[][text]", format_i18n_text_area_value(translation.text), :class => "locale", :id => "#{translation.object_id}_text", :onfocus => "$(this).up('tr').addClassName('active');", :onblur => "$(this).up('tr').removeClassName('active');" %>
28 | |
29 |
30 | <% if params[:q].present? -%>
31 | <%= highlight(format_i18n_value(phrase.translations.primary.text), params[:q]) -%>
32 | <% else -%>
33 | <%= format_i18n_value(phrase.translations.primary.text) -%>
34 | <% end -%>
35 | <%= params[:k].present? ?
36 | highlight(h(truncate(phrase.key, :length => 100)), params[:k]) :
37 | h(truncate(phrase.key, :length => 100)) %>
38 | |
39 |
40 | <% end %>
41 | <% end %>
42 |
43 |
44 |
<%= locale_form.submit "Save changes" %>
45 |
46 | <% end %>
47 |
48 | <%= will_paginate @phrases %>
49 |
50 | <% else %>
51 |
No search results.
52 | <% end %>
53 |
54 |
--------------------------------------------------------------------------------
/app/views/tolk/locales/all.html.erb:
--------------------------------------------------------------------------------
1 | Completed translations (<%= link_to 'See phrases missing translation', @locale %>)
2 |
3 | <% if @locale.has_updated_translations? && action_name != 'updated' %>
4 | Some phrases have changed. <%= link_to "Update translations", tolk.updated_locale_path(@locale) %>.
5 | <% end %>
6 |
7 |
8 | <%= render :partial => "tolk/searches/form", :locals => { :locale => @locale } %>
9 |
10 |
11 |
12 | <% if @phrases.any? %>
13 | <%= form_for @locale do |locale_form| %>
14 |
15 |
16 | | <%= @locale.language_name -%> |
17 | <%= Tolk::Locale.primary_language_name -%> |
18 |
19 | <% @phrases.each do |phrase| %>
20 | <% if phrase.translations.primary %>
21 |
22 | |
23 | <%= hidden_field_tag :"translations[][id]", phrase.translation.id %>
24 | <%= hidden_field_tag :"translations[][locale_id]", phrase.translation.locale_id %>
25 | <%= text_area_tag :"translations[][text]", format_i18n_text_area_value(phrase.translation.text), :class => 'locale',
26 | :onfocus => "$(this).up('tr').addClassName('active');", :onblur => "$(this).up('tr').removeClassName('active');" %>
27 | |
28 |
29 |
30 | <% if action_name == 'updated' %>
31 |
32 | Updated
33 | <%= format_i18n_value(phrase.translations.primary.text) -%>
34 |
35 |
36 | Original
37 | <%= format_i18n_value(phrase.translations.primary.previous_text) -%>
38 |
39 | <% else %>
40 | <%= format_i18n_value(phrase.translations.primary.text) -%>
41 | <% end %>
42 |
43 | <%= phrase.key %>
44 | |
45 |
46 | <% end %>
47 | <% end %>
48 |
49 |
50 |
<%= locale_form.submit "Save changes" %>
51 |
52 |
53 | <%= will_paginate @phrases %>
54 |
55 | <% end %>
56 | <% else %>
57 |
There aren't any completed translations for this locale.
58 |
59 | <% end %>
60 |
61 |
62 |
--------------------------------------------------------------------------------
/test/integration/translation_process_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class TranslationProcessTest < ActiveSupport::IntegrationCase
4 | setup :setup_locales
5 |
6 | def test_adding_locale
7 | assert_difference('Tolk::Locale.count') { add_locale 'German' }
8 | end
9 |
10 | def test_adding_missing_translations_and_updating_translations
11 | Tolk.config.mapping['xx'] = "Pirate"
12 |
13 | locale = add_locale("Pirate")
14 | assert locale.translations.empty?
15 |
16 | # Adding a new translation
17 | pirate_path = tolk.locale_path(locale)
18 | visit pirate_path
19 |
20 | fill_in_first_translation :with => "Dead men don't bite"
21 | click_button 'Save changes'
22 |
23 | assert_equal current_path, pirate_path
24 | assert_equal 1, locale.translations.count
25 |
26 | # Updating the translation added above
27 | click_link 'See completed translations'
28 | assert page.has_content?("Dead men don't bite")
29 |
30 | fill_in_first_translation :with => "Arrrr!"
31 | click_button 'Save changes'
32 |
33 | assert_equal current_path, tolk.all_locale_path(locale)
34 | assert_equal 1, locale.translations.count
35 | assert_equal 'Arrrr!', locale.translations(true).first.text
36 | end
37 |
38 | def test_search_phrase_within_key
39 | Tolk.config.mapping['xx'] = "Pirate"
40 | locale = add_locale("Pirate")
41 | assert locale.translations.empty?
42 |
43 | # Adding a new translation
44 | pirate_path = tolk.locale_path(locale)
45 | visit pirate_path
46 |
47 | fill_in 'q', :with => 'hello_country'
48 | fill_in 'k', :with => 'nested'
49 |
50 | click_button 'Search'
51 | assert_equal true, page.has_selector?('td.translation', :count => 1)
52 | end
53 |
54 | private
55 |
56 | def fill_in_first_translation(with_hash)
57 | within(:xpath, '//table[@class = "translations"]//tr[2]/td[@class = "translation"][1]') do
58 | fill_in 'translations[][text]', with_hash
59 | end
60 |
61 | end
62 |
63 | def add_locale(name)
64 | visit tolk.root_path
65 | select name, :from => "select_tolk_locale_name"
66 | click_button 'Add'
67 |
68 | Tolk::Locale.find_by_name!(Tolk.config.mapping.key(name))
69 | end
70 |
71 | def setup_locales
72 | Tolk::Locale.delete_all
73 | Tolk::Translation.delete_all
74 | Tolk::Phrase.delete_all
75 |
76 | Tolk::Locale.locales_config_path = File.join(Rails.root, "../locales/sync/")
77 |
78 | I18n.backend.reload!
79 | I18n.load_path = [Tolk::Locale.locales_config_path + 'en.yml']
80 | I18n.backend.send :init_translations
81 |
82 | Tolk::Locale.primary_locale(true)
83 |
84 | Tolk::Locale.sync!
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/app/views/tolk/locales/show.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :head do %>
2 |
3 | <% end %>
4 |
5 | Phrases missing translation (<%= @locale.count_phrases_without_translation %>) (<%= link_to 'See completed translations', tolk.all_locale_path(@locale) %>)
6 |
7 | <% if @locale.has_updated_translations? && action_name != 'updated' %>
8 | Some phrases have changed. <%= link_to "Update translations", tolk.updated_locale_path(@locale) %>.
9 | <% end %>
10 |
11 |
12 | <%= render :partial => "tolk/searches/form", :locals => { :locale => @locale } %>
13 |
14 |
15 |
16 | <% if @phrases.any? %>
17 | <%= form_for @locale do |locale_form| %>
18 |
19 |
20 | | <%= @locale.language_name -%> |
21 | <%= Tolk::Locale.primary_language_name -%> |
22 |
23 | <% @phrases.each do |phrase| %>
24 |
25 | <% translation = Tolk::Translation.new(:locale => @locale, :phrase => phrase) %>
26 | |
27 | <%= hidden_field_tag :"translations[][id]", translation.id, :id => "#{translation.object_id}_id" %>
28 | <%= hidden_field_tag :"translations[][phrase_id]", phrase.id, :id => "#{translation.object_id}_phrase_id" %>
29 | <%= hidden_field_tag :"translations[][locale_id]", translation.locale_id, :id => "#{translation.object_id}_locale_id" %>
30 | <%= text_area_tag :"translations[][text]", format_i18n_text_area_value(translation.text), :class => "locale", :id => "#{translation.object_id}_text", :onfocus => "$(this).up('tr').addClassName('active');", :onblur => "$(this).up('tr').removeClassName('active');" %>
31 | |
32 |
33 | <% if params[:q].present? -%>
34 | <%= highlight(format_i18n_value(phrase.translations.primary.text), params[:q]) -%>
35 | <% else -%>
36 | <%= format_i18n_value(phrase.translations.primary.text) -%>
37 | <% end -%>
38 | <%= truncate(phrase.key, :length => 100) %>
39 | |
40 |
41 | <% end %>
42 |
43 |
44 |
<%= locale_form.submit "Save changes" %>
45 |
46 | <% end %>
47 |
48 | <%= will_paginate @phrases %>
49 |
50 | <% else %>
51 |
There aren't any missing or updated phrases that need translation.
52 |
53 | <% end %>
54 |
55 |
--------------------------------------------------------------------------------
/test/unit/locale_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class LocaleTest < ActiveSupport::TestCase
4 | fixtures :all
5 |
6 | test "turning locale without nested phrases into a hash" do
7 | assert_equal({ "se" => { "hello_world" => "Hejsan Verdon" } }, tolk_locales(:se).to_hash)
8 | end
9 |
10 | test "turning locale with nested phrases into a hash" do
11 | assert_equal({ "en" => {
12 | "number"=>{"human"=>{"format"=>{"precision"=>1}}},
13 | "hello_world" => "Hello World",
14 | "nested" => {
15 | "hello_world" => "Nested Hello World",
16 | "hello_country" => "Nested Hello Country"
17 | }
18 | }}, tolk_locales(:en).to_hash)
19 | end
20 |
21 | test "phrases without translations" do
22 | assert tolk_locales(:en).phrases_without_translation.include?(tolk_phrases(:cozy))
23 | end
24 |
25 | test "searching phrases without translations" do
26 | assert !tolk_locales(:en).search_phrases_without_translation("cozy").include?(tolk_phrases(:hello_world))
27 | end
28 |
29 | test "paginating phrases without translations" do
30 | Tolk::Phrase.per_page = 2
31 | locale = tolk_locales(:se)
32 |
33 | page1 = locale.phrases_without_translation
34 | assert_equal [4, 3], page1.map(&:id)
35 |
36 | page2 = locale.phrases_without_translation(2)
37 | assert_equal [2, 5], page2.map(&:id)
38 |
39 | page3 = locale.phrases_without_translation(3)
40 | assert page3.blank?
41 | end
42 |
43 | test "paginating phrases with translations" do
44 | Tolk::Phrase.per_page = 4
45 | locale = tolk_locales(:en)
46 |
47 | page1 = locale.phrases_with_translation
48 | assert_equal [1, 3, 2, 5], page1.map(&:id)
49 |
50 | page2 = locale.phrases_with_translation(2)
51 | assert page2.blank?
52 | end
53 |
54 | test "counting missing translations" do
55 | assert_equal 2, tolk_locales(:da).count_phrases_without_translation
56 | assert_equal 4, tolk_locales(:se).count_phrases_without_translation
57 | end
58 |
59 | test "dumping all locales to yml" do
60 | Tolk::Locale.primary_locale_name = 'en'
61 | Tolk::Locale.primary_locale(true)
62 |
63 | begin
64 | FileUtils.mkdir_p(Rails.root.join("../../tmp/locales"))
65 | Tolk::Locale.dump_all(Rails.root.join("../../tmp/locales"))
66 |
67 | %w( da se ).each do |locale|
68 | assert_equal \
69 | File.read(Rails.root.join("../locales/basic/#{locale}.yml")),
70 | File.read(Rails.root.join("../../tmp/locales/#{locale}.yml"))
71 | end
72 |
73 | # Make sure dump doesn't generate en.yml
74 | assert ! File.exists?(Rails.root.join("../../tmp/locales/en.yml"))
75 | ensure
76 | FileUtils.rm_rf(Rails.root.join("../../tmp/locales"))
77 | end
78 | end
79 |
80 | test "human language name" do
81 | assert_equal 'English', tolk_locales(:en).language_name
82 | assert_equal 'pirate', Tolk::Locale.new(:name => 'pirate').language_name
83 | end
84 |
85 | end
86 |
--------------------------------------------------------------------------------
/lib/tolk/sync.rb:
--------------------------------------------------------------------------------
1 | module Tolk
2 | module Sync
3 | def self.included(base)
4 | base.send :extend, ClassMethods
5 | end
6 |
7 | module ClassMethods
8 | def sync!
9 | sync_phrases(load_translations)
10 | end
11 |
12 | def load_translations
13 | I18n.backend.send :init_translations unless I18n.backend.initialized? # force load
14 | translations = flat_hash(I18n.backend.send(:translations)[primary_locale.name.to_sym])
15 | filter_out_i18n_keys(translations.merge(read_primary_locale_file))
16 | end
17 |
18 | def read_primary_locale_file
19 | primary_file = "#{self.locales_config_path}/#{self.primary_locale_name}.yml"
20 | File.exists?(primary_file) ? flat_hash(YAML::load(IO.read(primary_file))[self.primary_locale_name]) : {}
21 | end
22 |
23 | def flat_hash(data, prefix = '', result = {})
24 | data.each do |key, value|
25 | current_prefix = prefix.present? ? "#{prefix}.#{key}" : key
26 |
27 | if !value.is_a?(Hash) || Tolk::Locale.pluralization_data?(value)
28 | result[current_prefix] = value.respond_to?(:stringify_keys) ? value.stringify_keys : value
29 | else
30 | flat_hash(value, current_prefix, result)
31 | end
32 | end
33 |
34 | result.stringify_keys
35 | end
36 |
37 | private
38 |
39 | def sync_phrases(translations)
40 | primary_locale = self.primary_locale
41 | secondary_locales = self.secondary_locales
42 |
43 | # Handle deleted phrases
44 | translations.present? ? Tolk::Phrase.destroy_all(["tolk_phrases.key NOT IN (?)", translations.keys]) : Tolk::Phrase.destroy_all
45 |
46 | phrases = Tolk::Phrase.all
47 |
48 | translations.each do |key, value|
49 | next if value.is_a?(Proc)
50 | # Create phrase and primary translation if missing
51 | existing_phrase = phrases.detect {|p| p.key == key} || Tolk::Phrase.create!(:key => key)
52 | translation = existing_phrase.translations.primary || primary_locale.translations.build(:phrase_id => existing_phrase.id)
53 | translation.text = value
54 |
55 | if translation.changed? && !translation.new_record?
56 | # Set the primary updated flag if the primary translation has changed and it is not a new record.
57 | secondary_locales.each do |locale|
58 | if existing_translation = existing_phrase.translations.detect {|t| t.locale_id == locale.id }
59 | existing_translation.force_set_primary_update = true
60 | existing_translation.save!
61 | end
62 | end
63 | end
64 |
65 | translation.primary = true
66 | translation.save!
67 | end
68 | end
69 |
70 | def filter_out_i18n_keys(flat_hash)
71 | flat_hash.reject { |key, value| key.starts_with? "i18n" }
72 | end
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tolk
2 | [](http://travis-ci.org/#!/tolk/tolk)
3 |
4 | [](https://gemnasium.com/tolk/tolk)
5 |
6 | This is now the official tolk repository.
7 |
8 | Tolk is a Rails 3 engine designed to facilitate the translators doing the dirty work of translating your application to other languages.
9 |
10 | ## Installation & Setup
11 |
12 | To install add the following to your Gemfile:
13 |
14 | ```ruby
15 | gem 'tolk'
16 | ```
17 |
18 | To setup just run:
19 |
20 | ```bash
21 | $ rake tolk:setup
22 | ```
23 |
24 | and follow the guide !
25 |
26 | ## Usage
27 |
28 | Tolk treats `I18n.default_locale` as the master source of strings to be translated. If you want the master source to be different from `I18n.default_locale`, you can override it by setting `Tolk::Locale.primary_locale_name`. Developers are expected to make all the changes to the master locale file ( en.yml by default ) and treat all the other locale.yml files as readonly files.
29 |
30 | As tolk stores all the keys and translated strings in the database, you need to ask Tolk to update it's database from the primary yml file :
31 |
32 | ```bash
33 | $ rake tolk:sync
34 | ```
35 |
36 | The above will fetch all the new keys from en.yml and put them in the database. Additionally, it'll also get rid of the deleted keys from the database and reflect updated translations - if any.
37 |
38 | If you already have data in your non primary locale files, you will need to import those to Tolk as a one time thing :
39 |
40 | ```bash
41 | $ rake tolk:import
42 | ```
43 |
44 | Upon visiting http://your_app.com/tolk - you will be presented with different options like creating new locale or providing translations for the existing locales. Once done with translating all the pending strings, you are can write back the new locales to filesystem :
45 |
46 | ```bash
47 | $ rake tolk:dump_all
48 | ```
49 |
50 | This will generate yml files for all non primary locales and put them in #{Rails.root}/config/locales/ directory by default.
51 |
52 | You can use the dump_all method defined in Tolk::Locale directly and pass directory path as the argument if you want the generated files to be at a different location :
53 |
54 | ```bash
55 | $ script/runner "Tolk::Locale.dump_all('/Users/lifo')"
56 | ```
57 |
58 | You can even download the yml file using Tolk web interface by appending '.yml' to the locale url. E.g http://your_app.com/tolk/locales/de.yml
59 |
60 | ## Authentication
61 |
62 | If you want to authenticate users who can access Tolk, you need to provide Tolk::ApplicationController.authenticator proc. For example :
63 |
64 | ```ruby
65 | # config/initializers/tolk.rb
66 | Tolk::ApplicationController.authenticator = proc {
67 | authenticate_or_request_with_http_basic do |user_name, password|
68 | user_name == 'translator' && password == 'transpass'
69 | end
70 | }
71 | ```
72 |
73 | Authenticator proc will be run from a before filter in controller context.
74 |
75 | ## Handling blank and non-string values
76 |
77 | Tolk speaks YAML for non strings values. If you want to enter a nil values, you could just enter '~'. Similarly, for an Array value, you could enter :
78 |
79 | ```yml
80 | ---
81 | - Sun
82 | - Mon
83 | ```
84 |
85 | And Tolk will take care of generating the appropriate entry in the YAML file.
86 |
--------------------------------------------------------------------------------
/app/models/tolk/translation.rb:
--------------------------------------------------------------------------------
1 | module Tolk
2 | class Translation < ActiveRecord::Base
3 | self.table_name = "tolk_translations"
4 |
5 | scope :containing_text, lambda {|query| where("tolk_translations.text LIKE ?", "%#{query}%") }
6 |
7 | serialize :text
8 | validates_presence_of :text, :if => proc {|r| r.primary.blank? && !r.explicit_nil }
9 | validate :check_matching_variables, :if => proc { |tr| tr.primary_translation.present? }
10 |
11 | validates_uniqueness_of :phrase_id, :scope => :locale_id
12 |
13 | belongs_to :phrase, :class_name => 'Tolk::Phrase'
14 | belongs_to :locale, :class_name => 'Tolk::Locale'
15 | validates_presence_of :locale_id
16 |
17 | attr_accessible :phrase_id, :locale_id, :text, :primary_updated, :previous_text, :locale, :phrase
18 |
19 | attr_accessor :force_set_primary_update
20 | before_save :set_primary_updated
21 |
22 | before_save :set_previous_text
23 |
24 | attr_accessor :primary
25 | before_validation :fix_text_type, :unless => proc {|r| r.primary }
26 |
27 | attr_accessor :explicit_nil
28 | before_validation :set_explicit_nil
29 |
30 | def up_to_date?
31 | not out_of_date?
32 | end
33 |
34 | def out_of_date?
35 | primary_updated?
36 | end
37 |
38 | def primary_translation
39 | @_primary_translation ||= begin
40 | if locale && !locale.primary?
41 | phrase.translations.primary
42 | end
43 | end
44 | end
45 |
46 | def text=(value)
47 | value = value.to_s if value.kind_of?(Fixnum)
48 | super unless value.to_s == text
49 | end
50 |
51 | def value
52 | if text.is_a?(String) && /^\d+$/.match(text)
53 | text.to_i
54 | else
55 | text
56 | end
57 | end
58 |
59 | def self.detect_variables(search_in)
60 | case search_in
61 | when String then Set.new(search_in.scan(/\{\{(\w+)\}\}/).flatten + search_in.scan(/\%\{(\w+)\}/).flatten)
62 | when Array then search_in.inject(Set[]) { |carry, item| carry + detect_variables(item) }
63 | when Hash then search_in.values.inject(Set[]) { |carry, item| carry + detect_variables(item) }
64 | else Set[]
65 | end
66 | end
67 |
68 | def variables
69 | self.class.detect_variables(text)
70 | end
71 |
72 | def variables_match?
73 | self.variables == primary_translation.variables
74 | end
75 |
76 | private
77 |
78 | def set_explicit_nil
79 | if self.text == '~'
80 | self.text = nil
81 | self.explicit_nil = true
82 | end
83 | end
84 |
85 | def fix_text_type
86 | if primary_translation.present?
87 | if self.text.is_a?(String) && !primary_translation.text.is_a?(String)
88 | self.text = begin
89 | YAML.load(self.text.strip)
90 | rescue ArgumentError
91 | nil
92 | end
93 | end
94 |
95 | self.text = nil if primary_translation.text.class != self.text.class
96 | end
97 |
98 | true
99 | end
100 |
101 | def set_primary_updated
102 | self.primary_updated = self.force_set_primary_update ? true : false
103 | true
104 | end
105 |
106 | def set_previous_text
107 | self.previous_text = self.text_was if text_changed?
108 | true
109 | end
110 |
111 | def check_matching_variables
112 | unless variables_match?
113 | if primary_translation.variables.empty? || primary_translation.variables.class == Set
114 | self.errors.add(:text, "The original does not contain variables, so they should not be included.")
115 | else
116 | self.errors.add(:text, "The translation should contain the variables #{primary_translation.to_a.to_sentence}.")
117 | end
118 | end
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/test/unit/format_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'fileutils'
3 |
4 | class FormatTest < ActiveSupport::TestCase
5 | def setup
6 | Tolk::Locale.delete_all
7 | Tolk::Translation.delete_all
8 | Tolk::Phrase.delete_all
9 |
10 | Tolk::Locale.locales_config_path = Rails.root.join("../locales/formats")
11 |
12 | I18n.backend.reload!
13 | I18n.load_path = [Tolk::Locale.locales_config_path + 'en.yml']
14 | I18n.backend.send :init_translations
15 |
16 | @en = Tolk::Locale.primary_locale(true)
17 | @spanish = Tolk::Locale.create!(:name => 'es')
18 |
19 | Tolk::Locale.sync!
20 | end
21 |
22 | def test_all_formats_are_loaded_properly
23 | # TODO : Investigate why the fuck does this test fail
24 | # assert_equal 1, @en['number']
25 | assert_equal 'I am just a stupid string :(', @en.get('string')
26 | assert_equal [1, 2, 3], @en.get('number_array')
27 | assert_equal ['sun', 'moon'], @en.get('string_array')
28 | end
29 |
30 | def test_pluaralization
31 | result = {'other' => 'Hello'}
32 | assert_equal result, @en.get('pluralization')
33 |
34 | assert ! @en.get('not_pluralization')
35 | assert_equal 'World', @en.get('not_pluralization.other')
36 | assert_equal 'fifo', @en.get('not_pluralization.lifo')
37 | end
38 |
39 | # def test_specail_activerecord_keys_and_prefixes
40 | # Special key
41 | # result = {'person' => 'Dude'}
42 | # assert_equal result, @en['activerecord.models']
43 |
44 | # Special prefix
45 | # result = {'login' => 'Handle'}
46 | # assert_equal result, @en['activerecord.attributes.person']
47 | # end
48 |
49 | def test_creating_translations_fails_on_mismatch_with_primary_translation
50 | # Invalid type
51 | assert_raises(ActiveRecord::RecordInvalid) { @spanish.translations.create!(:text => 'hola', :phrase => ph('number_array')) }
52 |
53 | # Invalid YAML
54 | assert_raises(ActiveRecord::RecordInvalid) { @spanish.translations.create!(:text => "1\n- 2\n", :phrase => ph('number_array')) }
55 |
56 | success = @spanish.translations.create!(:text => [1, 2], :phrase => ph('number_array'))
57 | assert_equal [1, 2], success.text
58 |
59 | success.text = "--- \n- 1\n- 2\n"
60 | success.save!
61 | assert_equal [1, 2], success.text
62 | end
63 |
64 | def test_creating_translations_fails_with_unmatching_variables
65 | # Check that variable detection works correctly
66 | assert_equal Set['hello', 'world'], ph('variables').translations.primary.variables
67 | assert_equal Set['more', 'variables'], ph('variables_in_struct').translations.primary.variables
68 |
69 | # Allow different ordering and multiple occurences of variables
70 | assert @spanish.translations.build(:text => '{{world}} y {{hello}} y {{hello}} y {{world}}', :phrase => ph('variables')).valid?
71 |
72 | # Do not allow missing or wrong variables
73 | assert_raises(ActiveRecord::RecordInvalid) { @spanish.translations.create!(:text => 'Hola', :phrase => ph('variables')) }
74 | assert_raises(ActiveRecord::RecordInvalid) { @spanish.translations.create!(:text => '{{other}} variable', :phrase => ph('variables')) }
75 |
76 | # Do not allow variables if the origin does not contain any
77 | assert_equal Set[], ph('string').translations.primary.variables
78 | assert_raises(ActiveRecord::RecordInvalid) { @spanish.translations.create!(:text => 'Hola {{mundo}}', :phrase => ph('string')) }
79 | end
80 |
81 | def test_creating_translations_with_nil_values
82 | # implicit nil value
83 | assert_raises(ActiveRecord::RecordInvalid) { @spanish.translations.create!(:phrase => ph('string')) }
84 |
85 | # explicit nil value
86 | niltrans = @spanish.translations.create!(:phrase => ph('string'), :text => '~')
87 | assert ! niltrans.text
88 | end
89 |
90 | def test_creating_translation_with_wrong_type
91 | assert_raises(ActiveRecord::RecordInvalid) { @spanish.translations.create!(:text => [1, 2], :phrase => ph('string')) }
92 |
93 | translation = @spanish.translations.create!(:text => "one more silly string", :phrase => ph('string'))
94 | assert_equal "one more silly string", translation.text
95 | end
96 |
97 | def test_bulk_translations_update_with_some_invalid_formats
98 | @spanish.translations_attributes = [
99 | {"locale_id" => @spanish.id, "phrase_id" => ph('string_array').id, "text" => 'invalid format'},
100 | {"locale_id" => @spanish.id, "phrase_id" => ph('string').id, "text" => 'spanish string'},
101 | {"locale_id" => @spanish.id, "phrase_id" => ph('number').id, "text" => '2'}
102 | ]
103 |
104 | assert_difference('Tolk::Translation.count', 2) { @spanish.save }
105 |
106 | @spanish.reload
107 |
108 | assert_equal 'spanish string', @spanish.get('string')
109 | assert_equal '2', @spanish.get('number')
110 | assert ! @spanish.get('string_array')
111 | end
112 |
113 | def test_bulk_update_saves_unchanged_record
114 | data = {"locale_id" => @spanish.id, "phrase_id" => ph('string').id, "text" => 'spanish string'}
115 | @spanish.translations_attributes = [data]
116 | @spanish.save!
117 |
118 | spanish_string = @spanish.translations.first
119 | spanish_string.force_set_primary_update = true
120 | spanish_string.save!
121 | assert spanish_string.primary_updated?
122 |
123 | @spanish.reload
124 |
125 | @spanish.translations_attributes = [data.merge('id' => spanish_string.id)]
126 | @spanish.save!
127 |
128 | spanish_string.reload
129 | assert ! spanish_string.primary_updated?
130 | end
131 |
132 | private
133 |
134 | def ph(key)
135 | Tolk::Phrase.all.detect {|p| p.key == key}
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/test/dummy/public/javascripts/rails.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | // Technique from Juriy Zaytsev
3 | // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
4 | function isEventSupported(eventName) {
5 | var el = document.createElement('div');
6 | eventName = 'on' + eventName;
7 | var isSupported = (eventName in el);
8 | if (!isSupported) {
9 | el.setAttribute(eventName, 'return;');
10 | isSupported = typeof el[eventName] == 'function';
11 | }
12 | el = null;
13 | return isSupported;
14 | }
15 |
16 | function isForm(element) {
17 | return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'
18 | }
19 |
20 | function isInput(element) {
21 | if (Object.isElement(element)) {
22 | var name = element.nodeName.toUpperCase()
23 | return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'
24 | }
25 | else return false
26 | }
27 |
28 | var submitBubbles = isEventSupported('submit'),
29 | changeBubbles = isEventSupported('change')
30 |
31 | if (!submitBubbles || !changeBubbles) {
32 | // augment the Event.Handler class to observe custom events when needed
33 | Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap(
34 | function(init, element, eventName, selector, callback) {
35 | init(element, eventName, selector, callback)
36 | // is the handler being attached to an element that doesn't support this event?
37 | if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) ||
38 | (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) {
39 | // "submit" => "emulated:submit"
40 | this.eventName = 'emulated:' + this.eventName
41 | }
42 | }
43 | )
44 | }
45 |
46 | if (!submitBubbles) {
47 | // discover forms on the page by observing focus events which always bubble
48 | document.on('focusin', 'form', function(focusEvent, form) {
49 | // special handler for the real "submit" event (one-time operation)
50 | if (!form.retrieve('emulated:submit')) {
51 | form.on('submit', function(submitEvent) {
52 | var emulated = form.fire('emulated:submit', submitEvent, true)
53 | // if custom event received preventDefault, cancel the real one too
54 | if (emulated.returnValue === false) submitEvent.preventDefault()
55 | })
56 | form.store('emulated:submit', true)
57 | }
58 | })
59 | }
60 |
61 | if (!changeBubbles) {
62 | // discover form inputs on the page
63 | document.on('focusin', 'input, select, texarea', function(focusEvent, input) {
64 | // special handler for real "change" events
65 | if (!input.retrieve('emulated:change')) {
66 | input.on('change', function(changeEvent) {
67 | input.fire('emulated:change', changeEvent, true)
68 | })
69 | input.store('emulated:change', true)
70 | }
71 | })
72 | }
73 |
74 | function handleRemote(element) {
75 | var method, url, params;
76 |
77 | var event = element.fire("ajax:before");
78 | if (event.stopped) return false;
79 |
80 | if (element.tagName.toLowerCase() === 'form') {
81 | method = element.readAttribute('method') || 'post';
82 | url = element.readAttribute('action');
83 | params = element.serialize();
84 | } else {
85 | method = element.readAttribute('data-method') || 'get';
86 | url = element.readAttribute('href');
87 | params = {};
88 | }
89 |
90 | new Ajax.Request(url, {
91 | method: method,
92 | parameters: params,
93 | evalScripts: true,
94 |
95 | onComplete: function(request) { element.fire("ajax:complete", request); },
96 | onSuccess: function(request) { element.fire("ajax:success", request); },
97 | onFailure: function(request) { element.fire("ajax:failure", request); }
98 | });
99 |
100 | element.fire("ajax:after");
101 | }
102 |
103 | function handleMethod(element) {
104 | var method = element.readAttribute('data-method'),
105 | url = element.readAttribute('href'),
106 | csrf_param = $$('meta[name=csrf-param]')[0],
107 | csrf_token = $$('meta[name=csrf-token]')[0];
108 |
109 | var form = new Element('form', { method: "POST", action: url, style: "display: none;" });
110 | element.parentNode.insert(form);
111 |
112 | if (method !== 'post') {
113 | var field = new Element('input', { type: 'hidden', name: '_method', value: method });
114 | form.insert(field);
115 | }
116 |
117 | if (csrf_param) {
118 | var param = csrf_param.readAttribute('content'),
119 | token = csrf_token.readAttribute('content'),
120 | field = new Element('input', { type: 'hidden', name: param, value: token });
121 | form.insert(field);
122 | }
123 |
124 | form.submit();
125 | }
126 |
127 |
128 | document.on("click", "*[data-confirm]", function(event, element) {
129 | var message = element.readAttribute('data-confirm');
130 | if (!confirm(message)) event.stop();
131 | });
132 |
133 | document.on("click", "a[data-remote]", function(event, element) {
134 | if (event.stopped) return;
135 | handleRemote(element);
136 | event.stop();
137 | });
138 |
139 | document.on("click", "a[data-method]", function(event, element) {
140 | if (event.stopped) return;
141 | handleMethod(element);
142 | event.stop();
143 | });
144 |
145 | document.on("submit", function(event) {
146 | var element = event.findElement(),
147 | message = element.readAttribute('data-confirm');
148 | if (message && !confirm(message)) {
149 | event.stop();
150 | return false;
151 | }
152 |
153 | var inputs = element.select("input[type=submit][data-disable-with]");
154 | inputs.each(function(input) {
155 | input.disabled = true;
156 | input.writeAttribute('data-original-value', input.value);
157 | input.value = input.readAttribute('data-disable-with');
158 | });
159 |
160 | var element = event.findElement("form[data-remote]");
161 | if (element) {
162 | handleRemote(element);
163 | event.stop();
164 | }
165 | });
166 |
167 | document.on("ajax:after", "form", function(event, element) {
168 | var inputs = element.select("input[type=submit][disabled=true][data-disable-with]");
169 | inputs.each(function(input) {
170 | input.value = input.readAttribute('data-original-value');
171 | input.removeAttribute('data-original-value');
172 | input.disabled = false;
173 | });
174 | });
175 | })();
176 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/tolk/screen.css:
--------------------------------------------------------------------------------
1 | /*-------------------------------------------------
2 | Defaults
3 | -------------------------------------------------*/
4 |
5 | acronym, abbr {
6 | font-variant: small-caps;
7 | text-transform: lowercase;
8 | font-weight: bold;
9 | }
10 |
11 | .center {text-align: center;}
12 | .large {font-size: larger;}
13 | .small {font-size: smaller;}
14 | strong {font-weight: bold;}
15 | em {font-style: italic;}
16 |
17 | .clear {clear: both;}
18 | .clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; }
19 | .clearfix { display: inline-block; }
20 | .clearfix{ display: block; }
21 |
22 | a {color: #888;}
23 | a:hover {color: #000;}
24 |
25 | /*-------------------------------------------------
26 | Layout
27 | -------------------------------------------------*/
28 |
29 | body {
30 | font-family: "Lucida Sans", "Lucida Grande", "Lucida Sans Unicode", sans-serif;
31 | background: #e5e5e5;
32 | color: #333;
33 | margin: 0;
34 | padding: 0;
35 | font-size: 14px;
36 | line-height: 21px;
37 | text-align: left;
38 | }
39 |
40 | div#container {
41 | margin: 2% 4%;
42 | padding: 25px;
43 | background: #fff;
44 | -webkit-border-radius: 20px;
45 | -moz-border-radius: 20px;
46 | border-radius: 20px;
47 | box-shadow: 0px 0px 15px rgba(00,00,00,0.5);
48 | -moz-box-shadow: 0px 0px 15px rgba(00,00,00,0.5);
49 | -webkit-box-shadow: 0px 0px 15px rgba(00,00,00,0.5);
50 | }
51 |
52 | div#head {
53 | margin: -25px -25px 0;
54 | background: #111;
55 | color: #999;
56 | padding: 25px 25px 20px 15px;
57 | -webkit-border-top-left-radius: 20px;
58 | -webkit-border-top-right-radius: 20px;
59 | -moz-border-radius-topleft: 20px;
60 | -moz-border-radius-topright: 20px;
61 | border-top-left-radius: 20px;
62 | border-top-right-radius: 20px;
63 | font-size: 18px;
64 | }
65 |
66 | div#head a {
67 | color: #2fadcf;
68 | font-weight: bold;
69 | text-decoration: none;
70 | }
71 |
72 | div#head span {
73 | padding: 8px 6px;
74 | margin-right: 1px;
75 | }
76 |
77 | div#head span.home {
78 | background: #333;
79 | }
80 |
81 | div#head span.locale {
82 | background: #444;
83 | }
84 |
85 | div#head span.locale a {
86 | color: #fff;
87 | }
88 |
89 | div#head span:first-child {
90 | padding-left: 12px;
91 | -webkit-border-top-left-radius: 10px;
92 | -webkit-border-bottom-left-radius: 10px;
93 | -moz-border-radius-topleft: 10px;
94 | -moz-border-radius-bottomleft: 10px;
95 | border-top-left-radius: 10px;
96 | border-bottom-left-radius: 10px;
97 | }
98 |
99 | div#head span:last-child {
100 | padding-right: 12px;
101 | -webkit-border-top-right-radius: 10px;
102 | -webkit-border-bottom-right-radius: 10px;
103 | -moz-border-radius-topright: 10px;
104 | -moz-border-radius-bottomright: 10px;
105 | border-top-right-radius: 10px;
106 | border-bottom-right-radius: 10px;
107 | }
108 |
109 | h2,
110 | h3 {
111 | font-size: 18px;
112 | color: #2fadcf;
113 | margin: 25px 0 10px;
114 | }
115 |
116 | h2 span,
117 | h3 span {
118 | font-size: 13px;
119 | color: #888;
120 | }
121 |
122 | ul.locales {
123 | margin: 25px 0;
124 | }
125 |
126 | ul.locales li {
127 | width: 300px;
128 | display: block;
129 | float: left;
130 | margin: 0 10px 10px 0;
131 | /* background: #fff;
132 | -webkit-border-radius: 5px;
133 | -moz-border-radius: 5px;
134 | border-radius: 5px;
135 | border: 1px solid #e5e5e5;
136 | padding: 5px 10px;*/
137 | }
138 |
139 | ul.locales a {
140 | font-weight: bold;
141 | text-decoration: underline;
142 | color: #2fadcf;
143 | }
144 |
145 | /*ul.locales li:hover {
146 | background: #f5f5f5;
147 | border-color: #ccc;
148 | }*/
149 |
150 | ul.locales span.missing_translations {
151 | color: #fff;
152 | font-weight: bold;
153 | background: #c00;
154 | font-size: 9px;
155 | padding: 3px;
156 | -webkit-border-radius: 3px;
157 | -moz-border-radius: 3px;
158 | border-radius: 3px;
159 | line-height: 9px;
160 | vertical-align: top;
161 | display: inline-block;
162 | }
163 |
164 | ul.locales span {
165 | font-size: 13px;
166 | color: #666;
167 | }
168 |
169 | div.submit {
170 | background: #f5f5f5;
171 | margin: 25px -25px -25px;
172 | padding: 15px 25px;
173 | border-top: 1px dashed #ccc;
174 | -webkit-border-bottom-right-radius: 20px;
175 | -webkit-border-bottom-left-radius: 20px;
176 | -moz-border-radius-bottomright: 20px;
177 | -moz-border-radius-bottomleft: 20px;
178 | border-bottom-right-radius: 20px;
179 | border-bottom-left-radius: 20px;
180 | }
181 |
182 | span.updated {
183 | font-size: 11px;
184 | padding: 1px;
185 | background: #ffc;
186 | color: #777;
187 | margin-bottom: 10px;
188 | float: right;
189 | }
190 |
191 | div.table_submit {
192 | background: #f5f5f5;
193 | margin: -25px 0 0;
194 | padding: 12px 15px 15px;
195 | text-align: left;
196 | }
197 |
198 | div.translations {
199 | width: 96%;
200 | text-align: center;
201 | }
202 |
203 | span.notice {
204 | background: #ffc;
205 | color: #666;
206 | font-size: 12px;
207 | padding: 2px 5px;
208 | margin: -5px 0 15px;
209 | display: inline-block;
210 | }
211 |
212 | div.original {
213 | color: #999;
214 | margin: 5px 0;
215 | padding: 1px 8px 4px;
216 | }
217 |
218 | div.updated {
219 | background: #ffc;
220 | padding: 1px 8px 4px;
221 | }
222 |
223 | table.translations div.original span.key {
224 | margin: 0 0 -2px;
225 | padding: 0;
226 | }
227 |
228 | table.translations div.updated span.key {
229 | margin: 0 0 -2px;
230 | color: orange !important;
231 | padding: 0;
232 | }
233 |
234 | /*-------------------------------------------------
235 | Translation tables
236 | -------------------------------------------------*/
237 |
238 | table.translations {
239 | margin: 0 0 25px;
240 | width: 100%;
241 | text-align: left;
242 | }
243 |
244 | table.translations td,
245 | table.translations th {
246 | font-size: 14px;
247 | color: #222;
248 | padding: 12px 8px;
249 | border-bottom: 1px solid #e5e5e5;
250 | vertical-align: top;
251 | width: 50%;
252 | }
253 |
254 | table.translations th {
255 | border-bottom-color: #bbb;
256 | font-size: 11px;
257 | font-weight: bold;
258 | text-transform: uppercase;
259 | color: #999;
260 | padding-bottom: 2px;
261 | }
262 |
263 | table.translations textarea.locale {
264 | font-family: "Lucida Sans", "Lucida Grande", "Lucida Sans Unicode", sans-serif;
265 | font-size: 14px;
266 | line-height: 21px;
267 | color: #222;
268 | padding: 1px;
269 | width: 100%;
270 | height: 42px;
271 | }
272 |
273 | table.translations span.key {
274 | color: #aaa;
275 | font-size: 9px;
276 | display: block;
277 | }
278 |
279 | table.translations td.translation {
280 | padding: 10px 8px;
281 | }
282 |
283 | table.translations tr.active td {
284 | background: #edf9fe;
285 | }
286 |
287 | table.translations .highlight {
288 | background-color: yellow;
289 | }
290 |
291 | table.translations .phrase .carriage_return {
292 | color: #2fadcf;
293 | font-weight: bold;
294 | }
295 |
296 | /*-------------------------------------------------
297 | Pagination
298 | -------------------------------------------------*/
299 |
300 | div.paginate {
301 | margin: 15px auto 20px;
302 | font-size: 12px;
303 | color: #777;
304 | }
305 |
306 | div.paginate a,
307 | div.paginate span {
308 | padding: 2px 6px;
309 | text-decoration: none;
310 | }
311 |
312 | div.paginate a:hover,
313 | div.paginate span.current {
314 | -webkit-border-radius: 5px;
315 | -moz-border-radius: 5px;
316 | border-radius: 5px;
317 | background: #eee;
318 | color: #333;
319 | }
320 |
321 | div.paginate .next_page,
322 | div.paginate .prev_page {
323 | margin: 0 15px;
324 | -webkit-border-radius: 5px;
325 | -moz-border-radius: 5px;
326 | border-radius: 5px;
327 | border: 1px solid #bbb;
328 | padding: 4px 8px;
329 | }
330 |
331 | div.paginate .disabled {
332 | color: #ccc;
333 | border-color: #eee;
334 | }
335 |
--------------------------------------------------------------------------------
/app/models/tolk/locale.rb:
--------------------------------------------------------------------------------
1 | require 'tolk/config'
2 |
3 | module Tolk
4 | class Locale < ActiveRecord::Base
5 | self.table_name = "tolk_locales"
6 |
7 | def self._dump_path
8 | # Necessary to acces rails.root at runtime !
9 | @dump_path ||= Tolk.config.dump_path.is_a?(Proc) ? instance_eval(&Tolk.config.dump_path) : Tolk.config.dump_path
10 | end
11 |
12 | has_many :phrases, :through => :translations, :class_name => 'Tolk::Phrase'
13 | has_many :translations, :class_name => 'Tolk::Translation', :dependent => :destroy
14 |
15 | accepts_nested_attributes_for :translations, :reject_if => proc { |attributes| attributes['text'].blank? }
16 | before_validation :remove_invalid_translations_from_target, :on => :update
17 |
18 | attr_accessible :name
19 | cattr_accessor :locales_config_path
20 | self.locales_config_path = self._dump_path
21 |
22 | cattr_accessor :primary_locale_name
23 | self.primary_locale_name = I18n.default_locale.to_s
24 |
25 | include Tolk::Sync
26 | include Tolk::Import
27 |
28 | validates_uniqueness_of :name
29 | validates_presence_of :name
30 |
31 | cattr_accessor :special_prefixes
32 | self.special_prefixes = ['activerecord.attributes']
33 |
34 | cattr_accessor :special_keys
35 | self.special_keys = ['activerecord.models']
36 |
37 | class << self
38 | def primary_locale(reload = false)
39 | @_primary_locale = nil if reload
40 | @_primary_locale ||= begin
41 | raise "Primary locale is not set. Please set Locale.primary_locale_name in your application's config file" unless self.primary_locale_name
42 | find_or_create_by_name(self.primary_locale_name)
43 | end
44 | end
45 |
46 | def primary_language_name
47 | primary_locale.language_name
48 | end
49 |
50 | def secondary_locales
51 | all - [primary_locale]
52 | end
53 |
54 | def dump_all(to = self.locales_config_path)
55 | secondary_locales.each do |locale|
56 | File.open("#{to}/#{locale.name}.yml", "w+") do |file|
57 | data = locale.to_hash
58 | data.respond_to?(:ya2yaml) ? file.write(data.ya2yaml(:syck_compatible => true)) : YAML.dump(locale.to_hash, file)
59 | end
60 | end
61 | end
62 |
63 | def special_key_or_prefix?(prefix, key)
64 | self.special_prefixes.include?(prefix) || self.special_keys.include?(key)
65 | end
66 |
67 | PLURALIZATION_KEYS = ['none', 'one', 'two', 'few', 'many', 'other']
68 | def pluralization_data?(data)
69 | keys = data.keys.map(&:to_s)
70 | keys.all? {|k| PLURALIZATION_KEYS.include?(k) }
71 | end
72 | end
73 |
74 | def has_updated_translations?
75 | translations.count(:conditions => {:'tolk_translations.primary_updated' => true}) > 0
76 | end
77 |
78 | def phrases_with_translation(page = nil)
79 | find_phrases_with_translations(page, :'tolk_translations.primary_updated' => false)
80 | end
81 |
82 | def phrases_with_updated_translation(page = nil)
83 | find_phrases_with_translations(page, :'tolk_translations.primary_updated' => true)
84 | end
85 |
86 | def count_phrases_without_translation
87 | existing_ids = self.translations.all(:select => 'tolk_translations.phrase_id').map(&:phrase_id).uniq
88 | Tolk::Phrase.count - existing_ids.count
89 | end
90 |
91 | def phrases_without_translation(page = nil, options = {})
92 | phrases = Tolk::Phrase.scoped(:order => 'tolk_phrases.key ASC')
93 |
94 | existing_ids = self.translations.all(:select => 'tolk_translations.phrase_id').map(&:phrase_id).uniq
95 | phrases = phrases.scoped(:conditions => ['tolk_phrases.id NOT IN (?)', existing_ids]) if existing_ids.present?
96 |
97 | result = phrases.paginate({:page => page, :per_page => Phrase.per_page}.merge(options))
98 | ActiveRecord::Associations::Preloader.new result, :translations
99 | result
100 | end
101 |
102 | def search_phrases(query, scope, key_query, page = nil, options = {})
103 | return [] unless query.present? || key_query.present?
104 |
105 | translations = case scope
106 | when :origin
107 | Tolk::Locale.primary_locale.translations.containing_text(query)
108 | else # :target
109 | self.translations.containing_text(query)
110 | end
111 |
112 | phrases = Tolk::Phrase.scoped(:order => 'tolk_phrases.key ASC')
113 | phrases = phrases.containing_text(key_query)
114 |
115 | phrases = phrases.scoped(:conditions => ['tolk_phrases.id IN(?)', translations.map(&:phrase_id).uniq])
116 | phrases.paginate({:page => page}.merge(options))
117 | end
118 |
119 | def search_phrases_without_translation(query, page = nil, options = {})
120 | return phrases_without_translation(page, options) unless query.present?
121 |
122 | phrases = Tolk::Phrase.scoped(:order => 'tolk_phrases.key ASC')
123 |
124 | found_translations_ids = Tolk::Locale.primary_locale.translations.all(:conditions => ["tolk_translations.text LIKE ?", "%#{query}%"], :select => 'tolk_translations.phrase_id').map(&:phrase_id).uniq
125 | existing_ids = self.translations.all(:select => 'tolk_translations.phrase_id').map(&:phrase_id).uniq
126 | phrases = phrases.scoped(:conditions => ['tolk_phrases.id NOT IN (?) AND tolk_phrases.id IN(?)', existing_ids, found_translations_ids]) if existing_ids.present?
127 |
128 | result = phrases.paginate({:page => page}.merge(options))
129 | ActiveRecord::Associations::Preloader.new result, :translations
130 | result
131 | end
132 |
133 | def to_hash
134 | { name => translations.each_with_object({}) do |translation, locale|
135 | if translation.phrase.key.include?(".")
136 | locale.deep_merge!(unsquish(translation.phrase.key, translation.value))
137 | else
138 | locale[translation.phrase.key] = translation.value
139 | end
140 | end }
141 | end
142 |
143 | def to_param
144 | name.parameterize
145 | end
146 |
147 | def primary?
148 | name == self.class.primary_locale_name
149 | end
150 |
151 | def language_name
152 | Tolk.config.mapping[self.name] || self.name
153 | end
154 |
155 | def get(key)
156 | if phrase = Tolk::Phrase.find_by_key(key)
157 | t = self.translations.where(:phrase_id => phrase.id).first
158 | t.text if t
159 | end
160 | end
161 |
162 | def translations_with_html
163 | translations = self.translations.all(:conditions => "tolk_translations.text LIKE '%>%' AND
164 | tolk_translations.text LIKE '%<%' AND tolk_phrases.key NOT LIKE '%_html'", :joins => :phrase)
165 | ActiveRecord::Associations::Preloader.new translations, :phrase
166 | translations
167 | end
168 |
169 | def self.rename(old_name, new_name)
170 | if old_name.blank? || new_name.blank?
171 | "You need to provide both names, aborting."
172 | else
173 | if locale = find_by_name(old_name)
174 | locale.name = new_name
175 | locale.save
176 | "Locale ' #{old_name}' was renamed '#{new_name}'"
177 | else
178 | "Locale with name '#{old_name}' not found."
179 | end
180 | end
181 | end
182 |
183 | private
184 |
185 |
186 | def remove_invalid_translations_from_target
187 | self.translations.target.dup.each do |t|
188 | unless t.valid?
189 | self.translations.target.delete(t)
190 | else
191 | t.updated_at = Time.current # Silly hax to fool autosave into saving the record
192 | end
193 | end
194 |
195 | true
196 | end
197 |
198 | def find_phrases_with_translations(page, conditions = {})
199 | result = Tolk::Phrase.paginate(:page => page,
200 | :conditions => { :'tolk_translations.locale_id' => self.id }.merge(conditions),
201 | :joins => :translations, :order => 'tolk_phrases.key ASC')
202 |
203 | result.each do |phrase|
204 | phrase.translation = phrase.translations.for(self)
205 | end
206 |
207 | ActiveRecord::Associations::Preloader.new result, :translations
208 |
209 | result
210 | end
211 |
212 | def unsquish(string, value)
213 | if string.is_a?(String)
214 | unsquish(string.split("."), value)
215 | elsif string.size == 1
216 | { string.first => value }
217 | else
218 | key = string[0]
219 | rest = string[1..-1]
220 | { key => unsquish(rest, value) }
221 | end
222 | end
223 | end
224 | end
225 |
--------------------------------------------------------------------------------
/test/unit/sync_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'fileutils'
3 |
4 | class SyncTest < ActiveSupport::TestCase
5 | def setup
6 | Tolk::Locale.delete_all
7 | Tolk::Translation.delete_all
8 | Tolk::Phrase.delete_all
9 |
10 | Tolk::Locale.locales_config_path = Rails.root.join("../locales/sync/")
11 |
12 | I18n.backend.reload!
13 | I18n.load_path = [Tolk::Locale.locales_config_path + 'en.yml']
14 | I18n.backend.send :init_translations
15 |
16 | Tolk::Locale.primary_locale(true)
17 | end
18 |
19 | def test_flat_hash
20 | data = {'home' => {'hello' => 'hola', 'sidebar' => {'title' => 'something'}}}
21 | result = Tolk::Locale.send(:flat_hash, data)
22 |
23 | assert_equal 2, result.keys.size
24 | assert_equal ['home.hello', 'home.sidebar.title'], result.keys.sort
25 | assert_equal ['hola', 'something'], result.values.sort
26 | end
27 |
28 | def test_sync_sets_previous_text_for_primary_locale
29 | Tolk::Locale.expects(:load_translations).returns({"hello_world" => "Hello World"}).at_least_once
30 | Tolk::Locale.sync!
31 |
32 | # Change 'Hello World' to 'Hello Super World'
33 | Tolk::Locale.expects(:load_translations).returns({"hello_world" => "Hello Super World"}).at_least_once
34 | Tolk::Locale.sync!
35 |
36 | translation = Tolk::Locale.primary_locale(true).translations.first
37 | assert_equal 'Hello Super World', translation.text
38 | assert_equal 'Hello World', translation.previous_text
39 | end
40 |
41 | def test_sync_sets_primary_updated_for_secondary_translations_on_update
42 | spanish = Tolk::Locale.create!(:name => 'es')
43 |
44 | Tolk::Locale.expects(:load_translations).returns({"hello_world" => "Hello World", 'nested.hello_country' => 'Nested Hello Country'}).at_least_once
45 | Tolk::Locale.sync!
46 |
47 | phrase1 = Tolk::Phrase.all.detect {|p| p.key == 'hello_world'}
48 | t1 = spanish.translations.create!(:text => 'hola', :phrase => phrase1)
49 | phrase2 = Tolk::Phrase.all.detect {|p| p.key == 'nested.hello_country'}
50 | t2 = spanish.translations.create!(:text => 'nested hola', :phrase => phrase2)
51 |
52 | # Change 'Hello World' to 'Hello Super World'. But don't change nested.hello_country
53 | Tolk::Locale.expects(:load_translations).returns({'hello_world' => 'Hello Super World', 'nested.hello_country' => 'Nested Hello Country'}).at_least_once
54 | Tolk::Locale.sync!
55 |
56 | t1.reload
57 | t2.reload
58 |
59 | assert t1.primary_updated?
60 | assert ! t2.primary_updated?
61 | end
62 |
63 | def test_sync_marks_translations_for_review_when_the_primary_translation_has_changed
64 | Tolk::Locale.create!(:name => 'es')
65 |
66 | phrase = Tolk::Phrase.create! :key => 'number.precision'
67 | english_translation = phrase.translations.create!(:text => "1", :locale => Tolk::Locale.find_by_name("en"))
68 | spanish_translation = phrase.translations.create!(:text => "1", :locale => Tolk::Locale.find_by_name("es"))
69 |
70 | Tolk::Locale.expects(:load_translations).returns({'number.precision' => "1"}).at_least_once
71 | Tolk::Locale.sync! and spanish_translation.reload
72 | assert spanish_translation.up_to_date?
73 |
74 | Tolk::Locale.expects(:load_translations).returns({'number.precision' => "2"}).at_least_once
75 | Tolk::Locale.sync! and spanish_translation.reload
76 | assert spanish_translation.out_of_date?
77 |
78 | spanish_translation.text = "2"
79 | spanish_translation.save! and spanish_translation.reload
80 | assert spanish_translation.up_to_date?
81 |
82 | Tolk::Locale.expects(:load_translations).returns({'number.precision' => 2}).at_least_once
83 | Tolk::Locale.sync! and spanish_translation.reload
84 | assert spanish_translation.up_to_date?
85 |
86 | Tolk::Locale.expects(:load_translations).returns({'number.precision' => 1}).at_least_once
87 | Tolk::Locale.sync! and spanish_translation.reload
88 | assert spanish_translation.out_of_date?
89 | end
90 |
91 | def test_sync_creates_locale_phrases_translations
92 | Tolk::Locale.expects(:load_translations).returns({'hello_world' => 'Hello World', 'nested.hello_country' => 'Nested Hello Country'}).at_least_once
93 | Tolk::Locale.sync!
94 |
95 | # Created by sync!
96 | primary_locale = Tolk::Locale.find_by_name!(Tolk::Locale.primary_locale_name)
97 |
98 | assert_equal ["Hello World", "Nested Hello Country"], primary_locale.translations.map(&:text).sort
99 | assert_equal ["hello_world", "nested.hello_country"], Tolk::Phrase.all.map(&:key).sort
100 | end
101 |
102 | def test_sync_deletes_stale_translations_for_secondary_locales_on_delete_all
103 | spanish = Tolk::Locale.create!(:name => 'es')
104 |
105 | Tolk::Locale.expects(:load_translations).returns({'hello_world' => 'Hello World', 'nested.hello_country' => 'Nested Hello Country'}).at_least_once
106 | Tolk::Locale.sync!
107 |
108 | phrase = Tolk::Phrase.all.detect {|p| p.key == 'hello_world'}
109 | hola = spanish.translations.create!(:text => 'hola', :phrase => phrase)
110 |
111 | # Mimic deleting all the translations
112 | Tolk::Locale.expects(:load_translations).returns({}).at_least_once
113 | Tolk::Locale.sync!
114 |
115 | assert_equal 0, Tolk::Phrase.count
116 | assert_equal 0, Tolk::Translation.count
117 |
118 | assert_raises(ActiveRecord::RecordNotFound) { hola.reload }
119 | end
120 |
121 | def test_sync_deletes_stale_translations_for_secondary_locales_on_delete_some
122 | spanish = Tolk::Locale.create!(:name => 'es')
123 |
124 | Tolk::Locale.expects(:load_translations).returns({'hello_world' => 'Hello World', 'nested.hello_country' => 'Nested Hello Country'}).at_least_once
125 | Tolk::Locale.sync!
126 |
127 | phrase = Tolk::Phrase.all.detect {|p| p.key == 'hello_world'}
128 | hola = spanish.translations.create!(:text => 'hola', :phrase => phrase)
129 |
130 | # Mimic deleting 'hello_world'
131 | Tolk::Locale.expects(:load_translations).returns({'nested.hello_country' => 'Nested Hello World'}).at_least_once
132 | Tolk::Locale.sync!
133 |
134 | assert_equal 1, Tolk::Phrase.count
135 | assert_equal 1, Tolk::Translation.count
136 | assert_equal 0, spanish.translations.count
137 |
138 | assert_raises(ActiveRecord::RecordNotFound) { hola.reload }
139 | end
140 |
141 | def test_sync_handles_deleted_keys_and_updated_translations
142 | Tolk::Locale.sync!
143 |
144 | # Mimic deleting 'nested.hello_country' and updating 'hello_world'
145 | Tolk::Locale.expects(:load_translations).returns({"hello_world" => "Hello Super World"}).at_least_once
146 | Tolk::Locale.sync!
147 |
148 | primary_locale = Tolk::Locale.find_by_name!(Tolk::Locale.primary_locale_name)
149 |
150 | assert_equal ['Hello Super World'], primary_locale.translations.map(&:text)
151 | assert_equal ['hello_world'], Tolk::Phrase.all.map(&:key).sort
152 | end
153 |
154 | def test_sync_doesnt_mess_with_existing_translations
155 | spanish = Tolk::Locale.create!(:name => 'es')
156 |
157 | Tolk::Locale.expects(:load_translations).returns({"hello_world" => "Hello Super World"}).at_least_once
158 | Tolk::Locale.sync!
159 |
160 | phrase = Tolk::Phrase.all.detect {|p| p.key == 'hello_world'}
161 | hola = spanish.translations.create!(:text => 'hola', :phrase => phrase)
162 |
163 | # Mimic deleting 'nested.hello_country' and updating 'hello_world'
164 | Tolk::Locale.expects(:load_translations).returns({"hello_world" => "Hello Super World"}).at_least_once
165 | Tolk::Locale.sync!
166 |
167 | hola.reload
168 | assert_equal 'hola', hola.text
169 | end
170 |
171 | def test_sync_array_values
172 | spanish = Tolk::Locale.create!(:name => 'es')
173 |
174 | data = {"weekend" => ['Friday', 'Saturday', 'Sunday']}
175 | Tolk::Locale.expects(:load_translations).returns(data).at_least_once
176 | Tolk::Locale.sync!
177 |
178 | assert_equal 1, Tolk::Locale.primary_locale.translations.count
179 |
180 | translation = Tolk::Locale.primary_locale.translations.first
181 | assert_equal data['weekend'], translation.text
182 |
183 | yaml = ['Saturday', 'Sunday'].to_yaml
184 | spanish_weekends = spanish.translations.create!(:text => yaml, :phrase => Tolk::Phrase.first)
185 | assert_equal YAML.load(yaml), spanish_weekends.text
186 | end
187 |
188 | def test_dump_all_after_sync
189 | spanish = Tolk::Locale.create!(:name => 'es')
190 |
191 | Tolk::Locale.sync!
192 |
193 | phrase = Tolk::Phrase.all.detect {|p| p.key == 'hello_world'}
194 | hola = spanish.translations.create!(:text => 'hola', :phrase => phrase)
195 |
196 | tmpdir = Rails.root.join("../../tmp/sync/locales")
197 | FileUtils.mkdir_p(tmpdir)
198 | Tolk::Locale.dump_all(tmpdir)
199 |
200 | spanish_file = "#{tmpdir}/es.yml"
201 | data = YAML::load(IO.read(spanish_file))['es']
202 | assert_equal ['hello_world'], data.keys
203 | assert_equal 'hola', data['hello_world']
204 | ensure
205 | FileUtils.rm_f(tmpdir)
206 | end
207 | end
208 |
--------------------------------------------------------------------------------
/test/dummy/public/javascripts/dragdrop.js:
--------------------------------------------------------------------------------
1 | // script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
2 |
3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
4 | //
5 | // script.aculo.us is freely distributable under the terms of an MIT-style license.
6 | // For details, see the script.aculo.us web site: http://script.aculo.us/
7 |
8 | if(Object.isUndefined(Effect))
9 | throw("dragdrop.js requires including script.aculo.us' effects.js library");
10 |
11 | var Droppables = {
12 | drops: [],
13 |
14 | remove: function(element) {
15 | this.drops = this.drops.reject(function(d) { return d.element==$(element) });
16 | },
17 |
18 | add: function(element) {
19 | element = $(element);
20 | var options = Object.extend({
21 | greedy: true,
22 | hoverclass: null,
23 | tree: false
24 | }, arguments[1] || { });
25 |
26 | // cache containers
27 | if(options.containment) {
28 | options._containers = [];
29 | var containment = options.containment;
30 | if(Object.isArray(containment)) {
31 | containment.each( function(c) { options._containers.push($(c)) });
32 | } else {
33 | options._containers.push($(containment));
34 | }
35 | }
36 |
37 | if(options.accept) options.accept = [options.accept].flatten();
38 |
39 | Element.makePositioned(element); // fix IE
40 | options.element = element;
41 |
42 | this.drops.push(options);
43 | },
44 |
45 | findDeepestChild: function(drops) {
46 | deepest = drops[0];
47 |
48 | for (i = 1; i < drops.length; ++i)
49 | if (Element.isParent(drops[i].element, deepest.element))
50 | deepest = drops[i];
51 |
52 | return deepest;
53 | },
54 |
55 | isContained: function(element, drop) {
56 | var containmentNode;
57 | if(drop.tree) {
58 | containmentNode = element.treeNode;
59 | } else {
60 | containmentNode = element.parentNode;
61 | }
62 | return drop._containers.detect(function(c) { return containmentNode == c });
63 | },
64 |
65 | isAffected: function(point, element, drop) {
66 | return (
67 | (drop.element!=element) &&
68 | ((!drop._containers) ||
69 | this.isContained(element, drop)) &&
70 | ((!drop.accept) ||
71 | (Element.classNames(element).detect(
72 | function(v) { return drop.accept.include(v) } ) )) &&
73 | Position.within(drop.element, point[0], point[1]) );
74 | },
75 |
76 | deactivate: function(drop) {
77 | if(drop.hoverclass)
78 | Element.removeClassName(drop.element, drop.hoverclass);
79 | this.last_active = null;
80 | },
81 |
82 | activate: function(drop) {
83 | if(drop.hoverclass)
84 | Element.addClassName(drop.element, drop.hoverclass);
85 | this.last_active = drop;
86 | },
87 |
88 | show: function(point, element) {
89 | if(!this.drops.length) return;
90 | var drop, affected = [];
91 |
92 | this.drops.each( function(drop) {
93 | if(Droppables.isAffected(point, element, drop))
94 | affected.push(drop);
95 | });
96 |
97 | if(affected.length>0)
98 | drop = Droppables.findDeepestChild(affected);
99 |
100 | if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
101 | if (drop) {
102 | Position.within(drop.element, point[0], point[1]);
103 | if(drop.onHover)
104 | drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
105 |
106 | if (drop != this.last_active) Droppables.activate(drop);
107 | }
108 | },
109 |
110 | fire: function(event, element) {
111 | if(!this.last_active) return;
112 | Position.prepare();
113 |
114 | if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
115 | if (this.last_active.onDrop) {
116 | this.last_active.onDrop(element, this.last_active.element, event);
117 | return true;
118 | }
119 | },
120 |
121 | reset: function() {
122 | if(this.last_active)
123 | this.deactivate(this.last_active);
124 | }
125 | };
126 |
127 | var Draggables = {
128 | drags: [],
129 | observers: [],
130 |
131 | register: function(draggable) {
132 | if(this.drags.length == 0) {
133 | this.eventMouseUp = this.endDrag.bindAsEventListener(this);
134 | this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
135 | this.eventKeypress = this.keyPress.bindAsEventListener(this);
136 |
137 | Event.observe(document, "mouseup", this.eventMouseUp);
138 | Event.observe(document, "mousemove", this.eventMouseMove);
139 | Event.observe(document, "keypress", this.eventKeypress);
140 | }
141 | this.drags.push(draggable);
142 | },
143 |
144 | unregister: function(draggable) {
145 | this.drags = this.drags.reject(function(d) { return d==draggable });
146 | if(this.drags.length == 0) {
147 | Event.stopObserving(document, "mouseup", this.eventMouseUp);
148 | Event.stopObserving(document, "mousemove", this.eventMouseMove);
149 | Event.stopObserving(document, "keypress", this.eventKeypress);
150 | }
151 | },
152 |
153 | activate: function(draggable) {
154 | if(draggable.options.delay) {
155 | this._timeout = setTimeout(function() {
156 | Draggables._timeout = null;
157 | window.focus();
158 | Draggables.activeDraggable = draggable;
159 | }.bind(this), draggable.options.delay);
160 | } else {
161 | window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
162 | this.activeDraggable = draggable;
163 | }
164 | },
165 |
166 | deactivate: function() {
167 | this.activeDraggable = null;
168 | },
169 |
170 | updateDrag: function(event) {
171 | if(!this.activeDraggable) return;
172 | var pointer = [Event.pointerX(event), Event.pointerY(event)];
173 | // Mozilla-based browsers fire successive mousemove events with
174 | // the same coordinates, prevent needless redrawing (moz bug?)
175 | if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
176 | this._lastPointer = pointer;
177 |
178 | this.activeDraggable.updateDrag(event, pointer);
179 | },
180 |
181 | endDrag: function(event) {
182 | if(this._timeout) {
183 | clearTimeout(this._timeout);
184 | this._timeout = null;
185 | }
186 | if(!this.activeDraggable) return;
187 | this._lastPointer = null;
188 | this.activeDraggable.endDrag(event);
189 | this.activeDraggable = null;
190 | },
191 |
192 | keyPress: function(event) {
193 | if(this.activeDraggable)
194 | this.activeDraggable.keyPress(event);
195 | },
196 |
197 | addObserver: function(observer) {
198 | this.observers.push(observer);
199 | this._cacheObserverCallbacks();
200 | },
201 |
202 | removeObserver: function(element) { // element instead of observer fixes mem leaks
203 | this.observers = this.observers.reject( function(o) { return o.element==element });
204 | this._cacheObserverCallbacks();
205 | },
206 |
207 | notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
208 | if(this[eventName+'Count'] > 0)
209 | this.observers.each( function(o) {
210 | if(o[eventName]) o[eventName](eventName, draggable, event);
211 | });
212 | if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
213 | },
214 |
215 | _cacheObserverCallbacks: function() {
216 | ['onStart','onEnd','onDrag'].each( function(eventName) {
217 | Draggables[eventName+'Count'] = Draggables.observers.select(
218 | function(o) { return o[eventName]; }
219 | ).length;
220 | });
221 | }
222 | };
223 |
224 | /*--------------------------------------------------------------------------*/
225 |
226 | var Draggable = Class.create({
227 | initialize: function(element) {
228 | var defaults = {
229 | handle: false,
230 | reverteffect: function(element, top_offset, left_offset) {
231 | var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
232 | new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
233 | queue: {scope:'_draggable', position:'end'}
234 | });
235 | },
236 | endeffect: function(element) {
237 | var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
238 | new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
239 | queue: {scope:'_draggable', position:'end'},
240 | afterFinish: function(){
241 | Draggable._dragging[element] = false
242 | }
243 | });
244 | },
245 | zindex: 1000,
246 | revert: false,
247 | quiet: false,
248 | scroll: false,
249 | scrollSensitivity: 20,
250 | scrollSpeed: 15,
251 | snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
252 | delay: 0
253 | };
254 |
255 | if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
256 | Object.extend(defaults, {
257 | starteffect: function(element) {
258 | element._opacity = Element.getOpacity(element);
259 | Draggable._dragging[element] = true;
260 | new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
261 | }
262 | });
263 |
264 | var options = Object.extend(defaults, arguments[1] || { });
265 |
266 | this.element = $(element);
267 |
268 | if(options.handle && Object.isString(options.handle))
269 | this.handle = this.element.down('.'+options.handle, 0);
270 |
271 | if(!this.handle) this.handle = $(options.handle);
272 | if(!this.handle) this.handle = this.element;
273 |
274 | if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
275 | options.scroll = $(options.scroll);
276 | this._isScrollChild = Element.childOf(this.element, options.scroll);
277 | }
278 |
279 | Element.makePositioned(this.element); // fix IE
280 |
281 | this.options = options;
282 | this.dragging = false;
283 |
284 | this.eventMouseDown = this.initDrag.bindAsEventListener(this);
285 | Event.observe(this.handle, "mousedown", this.eventMouseDown);
286 |
287 | Draggables.register(this);
288 | },
289 |
290 | destroy: function() {
291 | Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
292 | Draggables.unregister(this);
293 | },
294 |
295 | currentDelta: function() {
296 | return([
297 | parseInt(Element.getStyle(this.element,'left') || '0'),
298 | parseInt(Element.getStyle(this.element,'top') || '0')]);
299 | },
300 |
301 | initDrag: function(event) {
302 | if(!Object.isUndefined(Draggable._dragging[this.element]) &&
303 | Draggable._dragging[this.element]) return;
304 | if(Event.isLeftClick(event)) {
305 | // abort on form elements, fixes a Firefox issue
306 | var src = Event.element(event);
307 | if((tag_name = src.tagName.toUpperCase()) && (
308 | tag_name=='INPUT' ||
309 | tag_name=='SELECT' ||
310 | tag_name=='OPTION' ||
311 | tag_name=='BUTTON' ||
312 | tag_name=='TEXTAREA')) return;
313 |
314 | var pointer = [Event.pointerX(event), Event.pointerY(event)];
315 | var pos = this.element.cumulativeOffset();
316 | this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
317 |
318 | Draggables.activate(this);
319 | Event.stop(event);
320 | }
321 | },
322 |
323 | startDrag: function(event) {
324 | this.dragging = true;
325 | if(!this.delta)
326 | this.delta = this.currentDelta();
327 |
328 | if(this.options.zindex) {
329 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
330 | this.element.style.zIndex = this.options.zindex;
331 | }
332 |
333 | if(this.options.ghosting) {
334 | this._clone = this.element.cloneNode(true);
335 | this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
336 | if (!this._originallyAbsolute)
337 | Position.absolutize(this.element);
338 | this.element.parentNode.insertBefore(this._clone, this.element);
339 | }
340 |
341 | if(this.options.scroll) {
342 | if (this.options.scroll == window) {
343 | var where = this._getWindowScroll(this.options.scroll);
344 | this.originalScrollLeft = where.left;
345 | this.originalScrollTop = where.top;
346 | } else {
347 | this.originalScrollLeft = this.options.scroll.scrollLeft;
348 | this.originalScrollTop = this.options.scroll.scrollTop;
349 | }
350 | }
351 |
352 | Draggables.notify('onStart', this, event);
353 |
354 | if(this.options.starteffect) this.options.starteffect(this.element);
355 | },
356 |
357 | updateDrag: function(event, pointer) {
358 | if(!this.dragging) this.startDrag(event);
359 |
360 | if(!this.options.quiet){
361 | Position.prepare();
362 | Droppables.show(pointer, this.element);
363 | }
364 |
365 | Draggables.notify('onDrag', this, event);
366 |
367 | this.draw(pointer);
368 | if(this.options.change) this.options.change(this);
369 |
370 | if(this.options.scroll) {
371 | this.stopScrolling();
372 |
373 | var p;
374 | if (this.options.scroll == window) {
375 | with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
376 | } else {
377 | p = Position.page(this.options.scroll);
378 | p[0] += this.options.scroll.scrollLeft + Position.deltaX;
379 | p[1] += this.options.scroll.scrollTop + Position.deltaY;
380 | p.push(p[0]+this.options.scroll.offsetWidth);
381 | p.push(p[1]+this.options.scroll.offsetHeight);
382 | }
383 | var speed = [0,0];
384 | if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
385 | if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
386 | if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
387 | if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
388 | this.startScrolling(speed);
389 | }
390 |
391 | // fix AppleWebKit rendering
392 | if(Prototype.Browser.WebKit) window.scrollBy(0,0);
393 |
394 | Event.stop(event);
395 | },
396 |
397 | finishDrag: function(event, success) {
398 | this.dragging = false;
399 |
400 | if(this.options.quiet){
401 | Position.prepare();
402 | var pointer = [Event.pointerX(event), Event.pointerY(event)];
403 | Droppables.show(pointer, this.element);
404 | }
405 |
406 | if(this.options.ghosting) {
407 | if (!this._originallyAbsolute)
408 | Position.relativize(this.element);
409 | delete this._originallyAbsolute;
410 | Element.remove(this._clone);
411 | this._clone = null;
412 | }
413 |
414 | var dropped = false;
415 | if(success) {
416 | dropped = Droppables.fire(event, this.element);
417 | if (!dropped) dropped = false;
418 | }
419 | if(dropped && this.options.onDropped) this.options.onDropped(this.element);
420 | Draggables.notify('onEnd', this, event);
421 |
422 | var revert = this.options.revert;
423 | if(revert && Object.isFunction(revert)) revert = revert(this.element);
424 |
425 | var d = this.currentDelta();
426 | if(revert && this.options.reverteffect) {
427 | if (dropped == 0 || revert != 'failure')
428 | this.options.reverteffect(this.element,
429 | d[1]-this.delta[1], d[0]-this.delta[0]);
430 | } else {
431 | this.delta = d;
432 | }
433 |
434 | if(this.options.zindex)
435 | this.element.style.zIndex = this.originalZ;
436 |
437 | if(this.options.endeffect)
438 | this.options.endeffect(this.element);
439 |
440 | Draggables.deactivate(this);
441 | Droppables.reset();
442 | },
443 |
444 | keyPress: function(event) {
445 | if(event.keyCode!=Event.KEY_ESC) return;
446 | this.finishDrag(event, false);
447 | Event.stop(event);
448 | },
449 |
450 | endDrag: function(event) {
451 | if(!this.dragging) return;
452 | this.stopScrolling();
453 | this.finishDrag(event, true);
454 | Event.stop(event);
455 | },
456 |
457 | draw: function(point) {
458 | var pos = this.element.cumulativeOffset();
459 | if(this.options.ghosting) {
460 | var r = Position.realOffset(this.element);
461 | pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
462 | }
463 |
464 | var d = this.currentDelta();
465 | pos[0] -= d[0]; pos[1] -= d[1];
466 |
467 | if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
468 | pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
469 | pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
470 | }
471 |
472 | var p = [0,1].map(function(i){
473 | return (point[i]-pos[i]-this.offset[i])
474 | }.bind(this));
475 |
476 | if(this.options.snap) {
477 | if(Object.isFunction(this.options.snap)) {
478 | p = this.options.snap(p[0],p[1],this);
479 | } else {
480 | if(Object.isArray(this.options.snap)) {
481 | p = p.map( function(v, i) {
482 | return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
483 | } else {
484 | p = p.map( function(v) {
485 | return (v/this.options.snap).round()*this.options.snap }.bind(this));
486 | }
487 | }}
488 |
489 | var style = this.element.style;
490 | if((!this.options.constraint) || (this.options.constraint=='horizontal'))
491 | style.left = p[0] + "px";
492 | if((!this.options.constraint) || (this.options.constraint=='vertical'))
493 | style.top = p[1] + "px";
494 |
495 | if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
496 | },
497 |
498 | stopScrolling: function() {
499 | if(this.scrollInterval) {
500 | clearInterval(this.scrollInterval);
501 | this.scrollInterval = null;
502 | Draggables._lastScrollPointer = null;
503 | }
504 | },
505 |
506 | startScrolling: function(speed) {
507 | if(!(speed[0] || speed[1])) return;
508 | this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
509 | this.lastScrolled = new Date();
510 | this.scrollInterval = setInterval(this.scroll.bind(this), 10);
511 | },
512 |
513 | scroll: function() {
514 | var current = new Date();
515 | var delta = current - this.lastScrolled;
516 | this.lastScrolled = current;
517 | if(this.options.scroll == window) {
518 | with (this._getWindowScroll(this.options.scroll)) {
519 | if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
520 | var d = delta / 1000;
521 | this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
522 | }
523 | }
524 | } else {
525 | this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
526 | this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
527 | }
528 |
529 | Position.prepare();
530 | Droppables.show(Draggables._lastPointer, this.element);
531 | Draggables.notify('onDrag', this);
532 | if (this._isScrollChild) {
533 | Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
534 | Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
535 | Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
536 | if (Draggables._lastScrollPointer[0] < 0)
537 | Draggables._lastScrollPointer[0] = 0;
538 | if (Draggables._lastScrollPointer[1] < 0)
539 | Draggables._lastScrollPointer[1] = 0;
540 | this.draw(Draggables._lastScrollPointer);
541 | }
542 |
543 | if(this.options.change) this.options.change(this);
544 | },
545 |
546 | _getWindowScroll: function(w) {
547 | var T, L, W, H;
548 | with (w.document) {
549 | if (w.document.documentElement && documentElement.scrollTop) {
550 | T = documentElement.scrollTop;
551 | L = documentElement.scrollLeft;
552 | } else if (w.document.body) {
553 | T = body.scrollTop;
554 | L = body.scrollLeft;
555 | }
556 | if (w.innerWidth) {
557 | W = w.innerWidth;
558 | H = w.innerHeight;
559 | } else if (w.document.documentElement && documentElement.clientWidth) {
560 | W = documentElement.clientWidth;
561 | H = documentElement.clientHeight;
562 | } else {
563 | W = body.offsetWidth;
564 | H = body.offsetHeight;
565 | }
566 | }
567 | return { top: T, left: L, width: W, height: H };
568 | }
569 | });
570 |
571 | Draggable._dragging = { };
572 |
573 | /*--------------------------------------------------------------------------*/
574 |
575 | var SortableObserver = Class.create({
576 | initialize: function(element, observer) {
577 | this.element = $(element);
578 | this.observer = observer;
579 | this.lastValue = Sortable.serialize(this.element);
580 | },
581 |
582 | onStart: function() {
583 | this.lastValue = Sortable.serialize(this.element);
584 | },
585 |
586 | onEnd: function() {
587 | Sortable.unmark();
588 | if(this.lastValue != Sortable.serialize(this.element))
589 | this.observer(this.element)
590 | }
591 | });
592 |
593 | var Sortable = {
594 | SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
595 |
596 | sortables: { },
597 |
598 | _findRootElement: function(element) {
599 | while (element.tagName.toUpperCase() != "BODY") {
600 | if(element.id && Sortable.sortables[element.id]) return element;
601 | element = element.parentNode;
602 | }
603 | },
604 |
605 | options: function(element) {
606 | element = Sortable._findRootElement($(element));
607 | if(!element) return;
608 | return Sortable.sortables[element.id];
609 | },
610 |
611 | destroy: function(element){
612 | element = $(element);
613 | var s = Sortable.sortables[element.id];
614 |
615 | if(s) {
616 | Draggables.removeObserver(s.element);
617 | s.droppables.each(function(d){ Droppables.remove(d) });
618 | s.draggables.invoke('destroy');
619 |
620 | delete Sortable.sortables[s.element.id];
621 | }
622 | },
623 |
624 | create: function(element) {
625 | element = $(element);
626 | var options = Object.extend({
627 | element: element,
628 | tag: 'li', // assumes li children, override with tag: 'tagname'
629 | dropOnEmpty: false,
630 | tree: false,
631 | treeTag: 'ul',
632 | overlap: 'vertical', // one of 'vertical', 'horizontal'
633 | constraint: 'vertical', // one of 'vertical', 'horizontal', false
634 | containment: element, // also takes array of elements (or id's); or false
635 | handle: false, // or a CSS class
636 | only: false,
637 | delay: 0,
638 | hoverclass: null,
639 | ghosting: false,
640 | quiet: false,
641 | scroll: false,
642 | scrollSensitivity: 20,
643 | scrollSpeed: 15,
644 | format: this.SERIALIZE_RULE,
645 |
646 | // these take arrays of elements or ids and can be
647 | // used for better initialization performance
648 | elements: false,
649 | handles: false,
650 |
651 | onChange: Prototype.emptyFunction,
652 | onUpdate: Prototype.emptyFunction
653 | }, arguments[1] || { });
654 |
655 | // clear any old sortable with same element
656 | this.destroy(element);
657 |
658 | // build options for the draggables
659 | var options_for_draggable = {
660 | revert: true,
661 | quiet: options.quiet,
662 | scroll: options.scroll,
663 | scrollSpeed: options.scrollSpeed,
664 | scrollSensitivity: options.scrollSensitivity,
665 | delay: options.delay,
666 | ghosting: options.ghosting,
667 | constraint: options.constraint,
668 | handle: options.handle };
669 |
670 | if(options.starteffect)
671 | options_for_draggable.starteffect = options.starteffect;
672 |
673 | if(options.reverteffect)
674 | options_for_draggable.reverteffect = options.reverteffect;
675 | else
676 | if(options.ghosting) options_for_draggable.reverteffect = function(element) {
677 | element.style.top = 0;
678 | element.style.left = 0;
679 | };
680 |
681 | if(options.endeffect)
682 | options_for_draggable.endeffect = options.endeffect;
683 |
684 | if(options.zindex)
685 | options_for_draggable.zindex = options.zindex;
686 |
687 | // build options for the droppables
688 | var options_for_droppable = {
689 | overlap: options.overlap,
690 | containment: options.containment,
691 | tree: options.tree,
692 | hoverclass: options.hoverclass,
693 | onHover: Sortable.onHover
694 | };
695 |
696 | var options_for_tree = {
697 | onHover: Sortable.onEmptyHover,
698 | overlap: options.overlap,
699 | containment: options.containment,
700 | hoverclass: options.hoverclass
701 | };
702 |
703 | // fix for gecko engine
704 | Element.cleanWhitespace(element);
705 |
706 | options.draggables = [];
707 | options.droppables = [];
708 |
709 | // drop on empty handling
710 | if(options.dropOnEmpty || options.tree) {
711 | Droppables.add(element, options_for_tree);
712 | options.droppables.push(element);
713 | }
714 |
715 | (options.elements || this.findElements(element, options) || []).each( function(e,i) {
716 | var handle = options.handles ? $(options.handles[i]) :
717 | (options.handle ? $(e).select('.' + options.handle)[0] : e);
718 | options.draggables.push(
719 | new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
720 | Droppables.add(e, options_for_droppable);
721 | if(options.tree) e.treeNode = element;
722 | options.droppables.push(e);
723 | });
724 |
725 | if(options.tree) {
726 | (Sortable.findTreeElements(element, options) || []).each( function(e) {
727 | Droppables.add(e, options_for_tree);
728 | e.treeNode = element;
729 | options.droppables.push(e);
730 | });
731 | }
732 |
733 | // keep reference
734 | this.sortables[element.identify()] = options;
735 |
736 | // for onupdate
737 | Draggables.addObserver(new SortableObserver(element, options.onUpdate));
738 |
739 | },
740 |
741 | // return all suitable-for-sortable elements in a guaranteed order
742 | findElements: function(element, options) {
743 | return Element.findChildren(
744 | element, options.only, options.tree ? true : false, options.tag);
745 | },
746 |
747 | findTreeElements: function(element, options) {
748 | return Element.findChildren(
749 | element, options.only, options.tree ? true : false, options.treeTag);
750 | },
751 |
752 | onHover: function(element, dropon, overlap) {
753 | if(Element.isParent(dropon, element)) return;
754 |
755 | if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
756 | return;
757 | } else if(overlap>0.5) {
758 | Sortable.mark(dropon, 'before');
759 | if(dropon.previousSibling != element) {
760 | var oldParentNode = element.parentNode;
761 | element.style.visibility = "hidden"; // fix gecko rendering
762 | dropon.parentNode.insertBefore(element, dropon);
763 | if(dropon.parentNode!=oldParentNode)
764 | Sortable.options(oldParentNode).onChange(element);
765 | Sortable.options(dropon.parentNode).onChange(element);
766 | }
767 | } else {
768 | Sortable.mark(dropon, 'after');
769 | var nextElement = dropon.nextSibling || null;
770 | if(nextElement != element) {
771 | var oldParentNode = element.parentNode;
772 | element.style.visibility = "hidden"; // fix gecko rendering
773 | dropon.parentNode.insertBefore(element, nextElement);
774 | if(dropon.parentNode!=oldParentNode)
775 | Sortable.options(oldParentNode).onChange(element);
776 | Sortable.options(dropon.parentNode).onChange(element);
777 | }
778 | }
779 | },
780 |
781 | onEmptyHover: function(element, dropon, overlap) {
782 | var oldParentNode = element.parentNode;
783 | var droponOptions = Sortable.options(dropon);
784 |
785 | if(!Element.isParent(dropon, element)) {
786 | var index;
787 |
788 | var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
789 | var child = null;
790 |
791 | if(children) {
792 | var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
793 |
794 | for (index = 0; index < children.length; index += 1) {
795 | if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
796 | offset -= Element.offsetSize (children[index], droponOptions.overlap);
797 | } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
798 | child = index + 1 < children.length ? children[index + 1] : null;
799 | break;
800 | } else {
801 | child = children[index];
802 | break;
803 | }
804 | }
805 | }
806 |
807 | dropon.insertBefore(element, child);
808 |
809 | Sortable.options(oldParentNode).onChange(element);
810 | droponOptions.onChange(element);
811 | }
812 | },
813 |
814 | unmark: function() {
815 | if(Sortable._marker) Sortable._marker.hide();
816 | },
817 |
818 | mark: function(dropon, position) {
819 | // mark on ghosting only
820 | var sortable = Sortable.options(dropon.parentNode);
821 | if(sortable && !sortable.ghosting) return;
822 |
823 | if(!Sortable._marker) {
824 | Sortable._marker =
825 | ($('dropmarker') || Element.extend(document.createElement('DIV'))).
826 | hide().addClassName('dropmarker').setStyle({position:'absolute'});
827 | document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
828 | }
829 | var offsets = dropon.cumulativeOffset();
830 | Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
831 |
832 | if(position=='after')
833 | if(sortable.overlap == 'horizontal')
834 | Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
835 | else
836 | Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
837 |
838 | Sortable._marker.show();
839 | },
840 |
841 | _tree: function(element, options, parent) {
842 | var children = Sortable.findElements(element, options) || [];
843 |
844 | for (var i = 0; i < children.length; ++i) {
845 | var match = children[i].id.match(options.format);
846 |
847 | if (!match) continue;
848 |
849 | var child = {
850 | id: encodeURIComponent(match ? match[1] : null),
851 | element: element,
852 | parent: parent,
853 | children: [],
854 | position: parent.children.length,
855 | container: $(children[i]).down(options.treeTag)
856 | };
857 |
858 | /* Get the element containing the children and recurse over it */
859 | if (child.container)
860 | this._tree(child.container, options, child);
861 |
862 | parent.children.push (child);
863 | }
864 |
865 | return parent;
866 | },
867 |
868 | tree: function(element) {
869 | element = $(element);
870 | var sortableOptions = this.options(element);
871 | var options = Object.extend({
872 | tag: sortableOptions.tag,
873 | treeTag: sortableOptions.treeTag,
874 | only: sortableOptions.only,
875 | name: element.id,
876 | format: sortableOptions.format
877 | }, arguments[1] || { });
878 |
879 | var root = {
880 | id: null,
881 | parent: null,
882 | children: [],
883 | container: element,
884 | position: 0
885 | };
886 |
887 | return Sortable._tree(element, options, root);
888 | },
889 |
890 | /* Construct a [i] index for a particular node */
891 | _constructIndex: function(node) {
892 | var index = '';
893 | do {
894 | if (node.id) index = '[' + node.position + ']' + index;
895 | } while ((node = node.parent) != null);
896 | return index;
897 | },
898 |
899 | sequence: function(element) {
900 | element = $(element);
901 | var options = Object.extend(this.options(element), arguments[1] || { });
902 |
903 | return $(this.findElements(element, options) || []).map( function(item) {
904 | return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
905 | });
906 | },
907 |
908 | setSequence: function(element, new_sequence) {
909 | element = $(element);
910 | var options = Object.extend(this.options(element), arguments[2] || { });
911 |
912 | var nodeMap = { };
913 | this.findElements(element, options).each( function(n) {
914 | if (n.id.match(options.format))
915 | nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
916 | n.parentNode.removeChild(n);
917 | });
918 |
919 | new_sequence.each(function(ident) {
920 | var n = nodeMap[ident];
921 | if (n) {
922 | n[1].appendChild(n[0]);
923 | delete nodeMap[ident];
924 | }
925 | });
926 | },
927 |
928 | serialize: function(element) {
929 | element = $(element);
930 | var options = Object.extend(Sortable.options(element), arguments[1] || { });
931 | var name = encodeURIComponent(
932 | (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
933 |
934 | if (options.tree) {
935 | return Sortable.tree(element, arguments[1]).children.map( function (item) {
936 | return [name + Sortable._constructIndex(item) + "[id]=" +
937 | encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
938 | }).flatten().join('&');
939 | } else {
940 | return Sortable.sequence(element, arguments[1]).map( function(item) {
941 | return name + "[]=" + encodeURIComponent(item);
942 | }).join('&');
943 | }
944 | }
945 | };
946 |
947 | // Returns true if child is contained within element
948 | Element.isParent = function(child, element) {
949 | if (!child.parentNode || child == element) return false;
950 | if (child.parentNode == element) return true;
951 | return Element.isParent(child.parentNode, element);
952 | };
953 |
954 | Element.findChildren = function(element, only, recursive, tagName) {
955 | if(!element.hasChildNodes()) return null;
956 | tagName = tagName.toUpperCase();
957 | if(only) only = [only].flatten();
958 | var elements = [];
959 | $A(element.childNodes).each( function(e) {
960 | if(e.tagName && e.tagName.toUpperCase()==tagName &&
961 | (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
962 | elements.push(e);
963 | if(recursive) {
964 | var grandchildren = Element.findChildren(e, only, recursive, tagName);
965 | if(grandchildren) elements.push(grandchildren);
966 | }
967 | });
968 |
969 | return (elements.length>0 ? elements.flatten() : []);
970 | };
971 |
972 | Element.offsetSize = function (element, type) {
973 | return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
974 | };
--------------------------------------------------------------------------------
/public/javascripts/dragdrop.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 | // (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
3 | //
4 | // script.aculo.us is freely distributable under the terms of an MIT-style license.
5 | // For details, see the script.aculo.us web site: http://script.aculo.us/
6 |
7 | if(Object.isUndefined(Effect))
8 | throw("dragdrop.js requires including script.aculo.us' effects.js library");
9 |
10 | var Droppables = {
11 | drops: [],
12 |
13 | remove: function(element) {
14 | this.drops = this.drops.reject(function(d) { return d.element==$(element) });
15 | },
16 |
17 | add: function(element) {
18 | element = $(element);
19 | var options = Object.extend({
20 | greedy: true,
21 | hoverclass: null,
22 | tree: false
23 | }, arguments[1] || { });
24 |
25 | // cache containers
26 | if(options.containment) {
27 | options._containers = [];
28 | var containment = options.containment;
29 | if(Object.isArray(containment)) {
30 | containment.each( function(c) { options._containers.push($(c)) });
31 | } else {
32 | options._containers.push($(containment));
33 | }
34 | }
35 |
36 | if(options.accept) options.accept = [options.accept].flatten();
37 |
38 | Element.makePositioned(element); // fix IE
39 | options.element = element;
40 |
41 | this.drops.push(options);
42 | },
43 |
44 | findDeepestChild: function(drops) {
45 | deepest = drops[0];
46 |
47 | for (i = 1; i < drops.length; ++i)
48 | if (Element.isParent(drops[i].element, deepest.element))
49 | deepest = drops[i];
50 |
51 | return deepest;
52 | },
53 |
54 | isContained: function(element, drop) {
55 | var containmentNode;
56 | if(drop.tree) {
57 | containmentNode = element.treeNode;
58 | } else {
59 | containmentNode = element.parentNode;
60 | }
61 | return drop._containers.detect(function(c) { return containmentNode == c });
62 | },
63 |
64 | isAffected: function(point, element, drop) {
65 | return (
66 | (drop.element!=element) &&
67 | ((!drop._containers) ||
68 | this.isContained(element, drop)) &&
69 | ((!drop.accept) ||
70 | (Element.classNames(element).detect(
71 | function(v) { return drop.accept.include(v) } ) )) &&
72 | Position.within(drop.element, point[0], point[1]) );
73 | },
74 |
75 | deactivate: function(drop) {
76 | if(drop.hoverclass)
77 | Element.removeClassName(drop.element, drop.hoverclass);
78 | this.last_active = null;
79 | },
80 |
81 | activate: function(drop) {
82 | if(drop.hoverclass)
83 | Element.addClassName(drop.element, drop.hoverclass);
84 | this.last_active = drop;
85 | },
86 |
87 | show: function(point, element) {
88 | if(!this.drops.length) return;
89 | var drop, affected = [];
90 |
91 | this.drops.each( function(drop) {
92 | if(Droppables.isAffected(point, element, drop))
93 | affected.push(drop);
94 | });
95 |
96 | if(affected.length>0)
97 | drop = Droppables.findDeepestChild(affected);
98 |
99 | if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
100 | if (drop) {
101 | Position.within(drop.element, point[0], point[1]);
102 | if(drop.onHover)
103 | drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
104 |
105 | if (drop != this.last_active) Droppables.activate(drop);
106 | }
107 | },
108 |
109 | fire: function(event, element) {
110 | if(!this.last_active) return;
111 | Position.prepare();
112 |
113 | if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
114 | if (this.last_active.onDrop) {
115 | this.last_active.onDrop(element, this.last_active.element, event);
116 | return true;
117 | }
118 | },
119 |
120 | reset: function() {
121 | if(this.last_active)
122 | this.deactivate(this.last_active);
123 | }
124 | }
125 |
126 | var Draggables = {
127 | drags: [],
128 | observers: [],
129 |
130 | register: function(draggable) {
131 | if(this.drags.length == 0) {
132 | this.eventMouseUp = this.endDrag.bindAsEventListener(this);
133 | this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
134 | this.eventKeypress = this.keyPress.bindAsEventListener(this);
135 |
136 | Event.observe(document, "mouseup", this.eventMouseUp);
137 | Event.observe(document, "mousemove", this.eventMouseMove);
138 | Event.observe(document, "keypress", this.eventKeypress);
139 | }
140 | this.drags.push(draggable);
141 | },
142 |
143 | unregister: function(draggable) {
144 | this.drags = this.drags.reject(function(d) { return d==draggable });
145 | if(this.drags.length == 0) {
146 | Event.stopObserving(document, "mouseup", this.eventMouseUp);
147 | Event.stopObserving(document, "mousemove", this.eventMouseMove);
148 | Event.stopObserving(document, "keypress", this.eventKeypress);
149 | }
150 | },
151 |
152 | activate: function(draggable) {
153 | if(draggable.options.delay) {
154 | this._timeout = setTimeout(function() {
155 | Draggables._timeout = null;
156 | window.focus();
157 | Draggables.activeDraggable = draggable;
158 | }.bind(this), draggable.options.delay);
159 | } else {
160 | window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
161 | this.activeDraggable = draggable;
162 | }
163 | },
164 |
165 | deactivate: function() {
166 | this.activeDraggable = null;
167 | },
168 |
169 | updateDrag: function(event) {
170 | if(!this.activeDraggable) return;
171 | var pointer = [Event.pointerX(event), Event.pointerY(event)];
172 | // Mozilla-based browsers fire successive mousemove events with
173 | // the same coordinates, prevent needless redrawing (moz bug?)
174 | if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
175 | this._lastPointer = pointer;
176 |
177 | this.activeDraggable.updateDrag(event, pointer);
178 | },
179 |
180 | endDrag: function(event) {
181 | if(this._timeout) {
182 | clearTimeout(this._timeout);
183 | this._timeout = null;
184 | }
185 | if(!this.activeDraggable) return;
186 | this._lastPointer = null;
187 | this.activeDraggable.endDrag(event);
188 | this.activeDraggable = null;
189 | },
190 |
191 | keyPress: function(event) {
192 | if(this.activeDraggable)
193 | this.activeDraggable.keyPress(event);
194 | },
195 |
196 | addObserver: function(observer) {
197 | this.observers.push(observer);
198 | this._cacheObserverCallbacks();
199 | },
200 |
201 | removeObserver: function(element) { // element instead of observer fixes mem leaks
202 | this.observers = this.observers.reject( function(o) { return o.element==element });
203 | this._cacheObserverCallbacks();
204 | },
205 |
206 | notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
207 | if(this[eventName+'Count'] > 0)
208 | this.observers.each( function(o) {
209 | if(o[eventName]) o[eventName](eventName, draggable, event);
210 | });
211 | if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
212 | },
213 |
214 | _cacheObserverCallbacks: function() {
215 | ['onStart','onEnd','onDrag'].each( function(eventName) {
216 | Draggables[eventName+'Count'] = Draggables.observers.select(
217 | function(o) { return o[eventName]; }
218 | ).length;
219 | });
220 | }
221 | }
222 |
223 | /*--------------------------------------------------------------------------*/
224 |
225 | var Draggable = Class.create({
226 | initialize: function(element) {
227 | var defaults = {
228 | handle: false,
229 | reverteffect: function(element, top_offset, left_offset) {
230 | var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
231 | new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
232 | queue: {scope:'_draggable', position:'end'}
233 | });
234 | },
235 | endeffect: function(element) {
236 | var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
237 | new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
238 | queue: {scope:'_draggable', position:'end'},
239 | afterFinish: function(){
240 | Draggable._dragging[element] = false
241 | }
242 | });
243 | },
244 | zindex: 1000,
245 | revert: false,
246 | quiet: false,
247 | scroll: false,
248 | scrollSensitivity: 20,
249 | scrollSpeed: 15,
250 | snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
251 | delay: 0
252 | };
253 |
254 | if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
255 | Object.extend(defaults, {
256 | starteffect: function(element) {
257 | element._opacity = Element.getOpacity(element);
258 | Draggable._dragging[element] = true;
259 | new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
260 | }
261 | });
262 |
263 | var options = Object.extend(defaults, arguments[1] || { });
264 |
265 | this.element = $(element);
266 |
267 | if(options.handle && Object.isString(options.handle))
268 | this.handle = this.element.down('.'+options.handle, 0);
269 |
270 | if(!this.handle) this.handle = $(options.handle);
271 | if(!this.handle) this.handle = this.element;
272 |
273 | if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
274 | options.scroll = $(options.scroll);
275 | this._isScrollChild = Element.childOf(this.element, options.scroll);
276 | }
277 |
278 | Element.makePositioned(this.element); // fix IE
279 |
280 | this.options = options;
281 | this.dragging = false;
282 |
283 | this.eventMouseDown = this.initDrag.bindAsEventListener(this);
284 | Event.observe(this.handle, "mousedown", this.eventMouseDown);
285 |
286 | Draggables.register(this);
287 | },
288 |
289 | destroy: function() {
290 | Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
291 | Draggables.unregister(this);
292 | },
293 |
294 | currentDelta: function() {
295 | return([
296 | parseInt(Element.getStyle(this.element,'left') || '0'),
297 | parseInt(Element.getStyle(this.element,'top') || '0')]);
298 | },
299 |
300 | initDrag: function(event) {
301 | if(!Object.isUndefined(Draggable._dragging[this.element]) &&
302 | Draggable._dragging[this.element]) return;
303 | if(Event.isLeftClick(event)) {
304 | // abort on form elements, fixes a Firefox issue
305 | var src = Event.element(event);
306 | if((tag_name = src.tagName.toUpperCase()) && (
307 | tag_name=='INPUT' ||
308 | tag_name=='SELECT' ||
309 | tag_name=='OPTION' ||
310 | tag_name=='BUTTON' ||
311 | tag_name=='TEXTAREA')) return;
312 |
313 | var pointer = [Event.pointerX(event), Event.pointerY(event)];
314 | var pos = Position.cumulativeOffset(this.element);
315 | this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
316 |
317 | Draggables.activate(this);
318 | Event.stop(event);
319 | }
320 | },
321 |
322 | startDrag: function(event) {
323 | this.dragging = true;
324 | if(!this.delta)
325 | this.delta = this.currentDelta();
326 |
327 | if(this.options.zindex) {
328 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
329 | this.element.style.zIndex = this.options.zindex;
330 | }
331 |
332 | if(this.options.ghosting) {
333 | this._clone = this.element.cloneNode(true);
334 | this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
335 | if (!this.element._originallyAbsolute)
336 | Position.absolutize(this.element);
337 | this.element.parentNode.insertBefore(this._clone, this.element);
338 | }
339 |
340 | if(this.options.scroll) {
341 | if (this.options.scroll == window) {
342 | var where = this._getWindowScroll(this.options.scroll);
343 | this.originalScrollLeft = where.left;
344 | this.originalScrollTop = where.top;
345 | } else {
346 | this.originalScrollLeft = this.options.scroll.scrollLeft;
347 | this.originalScrollTop = this.options.scroll.scrollTop;
348 | }
349 | }
350 |
351 | Draggables.notify('onStart', this, event);
352 |
353 | if(this.options.starteffect) this.options.starteffect(this.element);
354 | },
355 |
356 | updateDrag: function(event, pointer) {
357 | if(!this.dragging) this.startDrag(event);
358 |
359 | if(!this.options.quiet){
360 | Position.prepare();
361 | Droppables.show(pointer, this.element);
362 | }
363 |
364 | Draggables.notify('onDrag', this, event);
365 |
366 | this.draw(pointer);
367 | if(this.options.change) this.options.change(this);
368 |
369 | if(this.options.scroll) {
370 | this.stopScrolling();
371 |
372 | var p;
373 | if (this.options.scroll == window) {
374 | with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
375 | } else {
376 | p = Position.page(this.options.scroll);
377 | p[0] += this.options.scroll.scrollLeft + Position.deltaX;
378 | p[1] += this.options.scroll.scrollTop + Position.deltaY;
379 | p.push(p[0]+this.options.scroll.offsetWidth);
380 | p.push(p[1]+this.options.scroll.offsetHeight);
381 | }
382 | var speed = [0,0];
383 | if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
384 | if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
385 | if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
386 | if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
387 | this.startScrolling(speed);
388 | }
389 |
390 | // fix AppleWebKit rendering
391 | if(Prototype.Browser.WebKit) window.scrollBy(0,0);
392 |
393 | Event.stop(event);
394 | },
395 |
396 | finishDrag: function(event, success) {
397 | this.dragging = false;
398 |
399 | if(this.options.quiet){
400 | Position.prepare();
401 | var pointer = [Event.pointerX(event), Event.pointerY(event)];
402 | Droppables.show(pointer, this.element);
403 | }
404 |
405 | if(this.options.ghosting) {
406 | if (!this.element._originallyAbsolute)
407 | Position.relativize(this.element);
408 | delete this.element._originallyAbsolute;
409 | Element.remove(this._clone);
410 | this._clone = null;
411 | }
412 |
413 | var dropped = false;
414 | if(success) {
415 | dropped = Droppables.fire(event, this.element);
416 | if (!dropped) dropped = false;
417 | }
418 | if(dropped && this.options.onDropped) this.options.onDropped(this.element);
419 | Draggables.notify('onEnd', this, event);
420 |
421 | var revert = this.options.revert;
422 | if(revert && Object.isFunction(revert)) revert = revert(this.element);
423 |
424 | var d = this.currentDelta();
425 | if(revert && this.options.reverteffect) {
426 | if (dropped == 0 || revert != 'failure')
427 | this.options.reverteffect(this.element,
428 | d[1]-this.delta[1], d[0]-this.delta[0]);
429 | } else {
430 | this.delta = d;
431 | }
432 |
433 | if(this.options.zindex)
434 | this.element.style.zIndex = this.originalZ;
435 |
436 | if(this.options.endeffect)
437 | this.options.endeffect(this.element);
438 |
439 | Draggables.deactivate(this);
440 | Droppables.reset();
441 | },
442 |
443 | keyPress: function(event) {
444 | if(event.keyCode!=Event.KEY_ESC) return;
445 | this.finishDrag(event, false);
446 | Event.stop(event);
447 | },
448 |
449 | endDrag: function(event) {
450 | if(!this.dragging) return;
451 | this.stopScrolling();
452 | this.finishDrag(event, true);
453 | Event.stop(event);
454 | },
455 |
456 | draw: function(point) {
457 | var pos = Position.cumulativeOffset(this.element);
458 | if(this.options.ghosting) {
459 | var r = Position.realOffset(this.element);
460 | pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
461 | }
462 |
463 | var d = this.currentDelta();
464 | pos[0] -= d[0]; pos[1] -= d[1];
465 |
466 | if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
467 | pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
468 | pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
469 | }
470 |
471 | var p = [0,1].map(function(i){
472 | return (point[i]-pos[i]-this.offset[i])
473 | }.bind(this));
474 |
475 | if(this.options.snap) {
476 | if(Object.isFunction(this.options.snap)) {
477 | p = this.options.snap(p[0],p[1],this);
478 | } else {
479 | if(Object.isArray(this.options.snap)) {
480 | p = p.map( function(v, i) {
481 | return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this))
482 | } else {
483 | p = p.map( function(v) {
484 | return (v/this.options.snap).round()*this.options.snap }.bind(this))
485 | }
486 | }}
487 |
488 | var style = this.element.style;
489 | if((!this.options.constraint) || (this.options.constraint=='horizontal'))
490 | style.left = p[0] + "px";
491 | if((!this.options.constraint) || (this.options.constraint=='vertical'))
492 | style.top = p[1] + "px";
493 |
494 | if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
495 | },
496 |
497 | stopScrolling: function() {
498 | if(this.scrollInterval) {
499 | clearInterval(this.scrollInterval);
500 | this.scrollInterval = null;
501 | Draggables._lastScrollPointer = null;
502 | }
503 | },
504 |
505 | startScrolling: function(speed) {
506 | if(!(speed[0] || speed[1])) return;
507 | this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
508 | this.lastScrolled = new Date();
509 | this.scrollInterval = setInterval(this.scroll.bind(this), 10);
510 | },
511 |
512 | scroll: function() {
513 | var current = new Date();
514 | var delta = current - this.lastScrolled;
515 | this.lastScrolled = current;
516 | if(this.options.scroll == window) {
517 | with (this._getWindowScroll(this.options.scroll)) {
518 | if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
519 | var d = delta / 1000;
520 | this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
521 | }
522 | }
523 | } else {
524 | this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
525 | this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
526 | }
527 |
528 | Position.prepare();
529 | Droppables.show(Draggables._lastPointer, this.element);
530 | Draggables.notify('onDrag', this);
531 | if (this._isScrollChild) {
532 | Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
533 | Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
534 | Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
535 | if (Draggables._lastScrollPointer[0] < 0)
536 | Draggables._lastScrollPointer[0] = 0;
537 | if (Draggables._lastScrollPointer[1] < 0)
538 | Draggables._lastScrollPointer[1] = 0;
539 | this.draw(Draggables._lastScrollPointer);
540 | }
541 |
542 | if(this.options.change) this.options.change(this);
543 | },
544 |
545 | _getWindowScroll: function(w) {
546 | var T, L, W, H;
547 | with (w.document) {
548 | if (w.document.documentElement && documentElement.scrollTop) {
549 | T = documentElement.scrollTop;
550 | L = documentElement.scrollLeft;
551 | } else if (w.document.body) {
552 | T = body.scrollTop;
553 | L = body.scrollLeft;
554 | }
555 | if (w.innerWidth) {
556 | W = w.innerWidth;
557 | H = w.innerHeight;
558 | } else if (w.document.documentElement && documentElement.clientWidth) {
559 | W = documentElement.clientWidth;
560 | H = documentElement.clientHeight;
561 | } else {
562 | W = body.offsetWidth;
563 | H = body.offsetHeight
564 | }
565 | }
566 | return { top: T, left: L, width: W, height: H };
567 | }
568 | });
569 |
570 | Draggable._dragging = { };
571 |
572 | /*--------------------------------------------------------------------------*/
573 |
574 | var SortableObserver = Class.create({
575 | initialize: function(element, observer) {
576 | this.element = $(element);
577 | this.observer = observer;
578 | this.lastValue = Sortable.serialize(this.element);
579 | },
580 |
581 | onStart: function() {
582 | this.lastValue = Sortable.serialize(this.element);
583 | },
584 |
585 | onEnd: function() {
586 | Sortable.unmark();
587 | if(this.lastValue != Sortable.serialize(this.element))
588 | this.observer(this.element)
589 | }
590 | });
591 |
592 | var Sortable = {
593 | SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
594 |
595 | sortables: { },
596 |
597 | _findRootElement: function(element) {
598 | while (element.tagName.toUpperCase() != "BODY") {
599 | if(element.id && Sortable.sortables[element.id]) return element;
600 | element = element.parentNode;
601 | }
602 | },
603 |
604 | options: function(element) {
605 | element = Sortable._findRootElement($(element));
606 | if(!element) return;
607 | return Sortable.sortables[element.id];
608 | },
609 |
610 | destroy: function(element){
611 | var s = Sortable.options(element);
612 |
613 | if(s) {
614 | Draggables.removeObserver(s.element);
615 | s.droppables.each(function(d){ Droppables.remove(d) });
616 | s.draggables.invoke('destroy');
617 |
618 | delete Sortable.sortables[s.element.id];
619 | }
620 | },
621 |
622 | create: function(element) {
623 | element = $(element);
624 | var options = Object.extend({
625 | element: element,
626 | tag: 'li', // assumes li children, override with tag: 'tagname'
627 | dropOnEmpty: false,
628 | tree: false,
629 | treeTag: 'ul',
630 | overlap: 'vertical', // one of 'vertical', 'horizontal'
631 | constraint: 'vertical', // one of 'vertical', 'horizontal', false
632 | containment: element, // also takes array of elements (or id's); or false
633 | handle: false, // or a CSS class
634 | only: false,
635 | delay: 0,
636 | hoverclass: null,
637 | ghosting: false,
638 | quiet: false,
639 | scroll: false,
640 | scrollSensitivity: 20,
641 | scrollSpeed: 15,
642 | format: this.SERIALIZE_RULE,
643 |
644 | // these take arrays of elements or ids and can be
645 | // used for better initialization performance
646 | elements: false,
647 | handles: false,
648 |
649 | onChange: Prototype.emptyFunction,
650 | onUpdate: Prototype.emptyFunction
651 | }, arguments[1] || { });
652 |
653 | // clear any old sortable with same element
654 | this.destroy(element);
655 |
656 | // build options for the draggables
657 | var options_for_draggable = {
658 | revert: true,
659 | quiet: options.quiet,
660 | scroll: options.scroll,
661 | scrollSpeed: options.scrollSpeed,
662 | scrollSensitivity: options.scrollSensitivity,
663 | delay: options.delay,
664 | ghosting: options.ghosting,
665 | constraint: options.constraint,
666 | handle: options.handle };
667 |
668 | if(options.starteffect)
669 | options_for_draggable.starteffect = options.starteffect;
670 |
671 | if(options.reverteffect)
672 | options_for_draggable.reverteffect = options.reverteffect;
673 | else
674 | if(options.ghosting) options_for_draggable.reverteffect = function(element) {
675 | element.style.top = 0;
676 | element.style.left = 0;
677 | };
678 |
679 | if(options.endeffect)
680 | options_for_draggable.endeffect = options.endeffect;
681 |
682 | if(options.zindex)
683 | options_for_draggable.zindex = options.zindex;
684 |
685 | // build options for the droppables
686 | var options_for_droppable = {
687 | overlap: options.overlap,
688 | containment: options.containment,
689 | tree: options.tree,
690 | hoverclass: options.hoverclass,
691 | onHover: Sortable.onHover
692 | }
693 |
694 | var options_for_tree = {
695 | onHover: Sortable.onEmptyHover,
696 | overlap: options.overlap,
697 | containment: options.containment,
698 | hoverclass: options.hoverclass
699 | }
700 |
701 | // fix for gecko engine
702 | Element.cleanWhitespace(element);
703 |
704 | options.draggables = [];
705 | options.droppables = [];
706 |
707 | // drop on empty handling
708 | if(options.dropOnEmpty || options.tree) {
709 | Droppables.add(element, options_for_tree);
710 | options.droppables.push(element);
711 | }
712 |
713 | (options.elements || this.findElements(element, options) || []).each( function(e,i) {
714 | var handle = options.handles ? $(options.handles[i]) :
715 | (options.handle ? $(e).select('.' + options.handle)[0] : e);
716 | options.draggables.push(
717 | new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
718 | Droppables.add(e, options_for_droppable);
719 | if(options.tree) e.treeNode = element;
720 | options.droppables.push(e);
721 | });
722 |
723 | if(options.tree) {
724 | (Sortable.findTreeElements(element, options) || []).each( function(e) {
725 | Droppables.add(e, options_for_tree);
726 | e.treeNode = element;
727 | options.droppables.push(e);
728 | });
729 | }
730 |
731 | // keep reference
732 | this.sortables[element.id] = options;
733 |
734 | // for onupdate
735 | Draggables.addObserver(new SortableObserver(element, options.onUpdate));
736 |
737 | },
738 |
739 | // return all suitable-for-sortable elements in a guaranteed order
740 | findElements: function(element, options) {
741 | return Element.findChildren(
742 | element, options.only, options.tree ? true : false, options.tag);
743 | },
744 |
745 | findTreeElements: function(element, options) {
746 | return Element.findChildren(
747 | element, options.only, options.tree ? true : false, options.treeTag);
748 | },
749 |
750 | onHover: function(element, dropon, overlap) {
751 | if(Element.isParent(dropon, element)) return;
752 |
753 | if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
754 | return;
755 | } else if(overlap>0.5) {
756 | Sortable.mark(dropon, 'before');
757 | if(dropon.previousSibling != element) {
758 | var oldParentNode = element.parentNode;
759 | element.style.visibility = "hidden"; // fix gecko rendering
760 | dropon.parentNode.insertBefore(element, dropon);
761 | if(dropon.parentNode!=oldParentNode)
762 | Sortable.options(oldParentNode).onChange(element);
763 | Sortable.options(dropon.parentNode).onChange(element);
764 | }
765 | } else {
766 | Sortable.mark(dropon, 'after');
767 | var nextElement = dropon.nextSibling || null;
768 | if(nextElement != element) {
769 | var oldParentNode = element.parentNode;
770 | element.style.visibility = "hidden"; // fix gecko rendering
771 | dropon.parentNode.insertBefore(element, nextElement);
772 | if(dropon.parentNode!=oldParentNode)
773 | Sortable.options(oldParentNode).onChange(element);
774 | Sortable.options(dropon.parentNode).onChange(element);
775 | }
776 | }
777 | },
778 |
779 | onEmptyHover: function(element, dropon, overlap) {
780 | var oldParentNode = element.parentNode;
781 | var droponOptions = Sortable.options(dropon);
782 |
783 | if(!Element.isParent(dropon, element)) {
784 | var index;
785 |
786 | var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
787 | var child = null;
788 |
789 | if(children) {
790 | var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
791 |
792 | for (index = 0; index < children.length; index += 1) {
793 | if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
794 | offset -= Element.offsetSize (children[index], droponOptions.overlap);
795 | } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
796 | child = index + 1 < children.length ? children[index + 1] : null;
797 | break;
798 | } else {
799 | child = children[index];
800 | break;
801 | }
802 | }
803 | }
804 |
805 | dropon.insertBefore(element, child);
806 |
807 | Sortable.options(oldParentNode).onChange(element);
808 | droponOptions.onChange(element);
809 | }
810 | },
811 |
812 | unmark: function() {
813 | if(Sortable._marker) Sortable._marker.hide();
814 | },
815 |
816 | mark: function(dropon, position) {
817 | // mark on ghosting only
818 | var sortable = Sortable.options(dropon.parentNode);
819 | if(sortable && !sortable.ghosting) return;
820 |
821 | if(!Sortable._marker) {
822 | Sortable._marker =
823 | ($('dropmarker') || Element.extend(document.createElement('DIV'))).
824 | hide().addClassName('dropmarker').setStyle({position:'absolute'});
825 | document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
826 | }
827 | var offsets = Position.cumulativeOffset(dropon);
828 | Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
829 |
830 | if(position=='after')
831 | if(sortable.overlap == 'horizontal')
832 | Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
833 | else
834 | Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
835 |
836 | Sortable._marker.show();
837 | },
838 |
839 | _tree: function(element, options, parent) {
840 | var children = Sortable.findElements(element, options) || [];
841 |
842 | for (var i = 0; i < children.length; ++i) {
843 | var match = children[i].id.match(options.format);
844 |
845 | if (!match) continue;
846 |
847 | var child = {
848 | id: encodeURIComponent(match ? match[1] : null),
849 | element: element,
850 | parent: parent,
851 | children: [],
852 | position: parent.children.length,
853 | container: $(children[i]).down(options.treeTag)
854 | }
855 |
856 | /* Get the element containing the children and recurse over it */
857 | if (child.container)
858 | this._tree(child.container, options, child)
859 |
860 | parent.children.push (child);
861 | }
862 |
863 | return parent;
864 | },
865 |
866 | tree: function(element) {
867 | element = $(element);
868 | var sortableOptions = this.options(element);
869 | var options = Object.extend({
870 | tag: sortableOptions.tag,
871 | treeTag: sortableOptions.treeTag,
872 | only: sortableOptions.only,
873 | name: element.id,
874 | format: sortableOptions.format
875 | }, arguments[1] || { });
876 |
877 | var root = {
878 | id: null,
879 | parent: null,
880 | children: [],
881 | container: element,
882 | position: 0
883 | }
884 |
885 | return Sortable._tree(element, options, root);
886 | },
887 |
888 | /* Construct a [i] index for a particular node */
889 | _constructIndex: function(node) {
890 | var index = '';
891 | do {
892 | if (node.id) index = '[' + node.position + ']' + index;
893 | } while ((node = node.parent) != null);
894 | return index;
895 | },
896 |
897 | sequence: function(element) {
898 | element = $(element);
899 | var options = Object.extend(this.options(element), arguments[1] || { });
900 |
901 | return $(this.findElements(element, options) || []).map( function(item) {
902 | return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
903 | });
904 | },
905 |
906 | setSequence: function(element, new_sequence) {
907 | element = $(element);
908 | var options = Object.extend(this.options(element), arguments[2] || { });
909 |
910 | var nodeMap = { };
911 | this.findElements(element, options).each( function(n) {
912 | if (n.id.match(options.format))
913 | nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
914 | n.parentNode.removeChild(n);
915 | });
916 |
917 | new_sequence.each(function(ident) {
918 | var n = nodeMap[ident];
919 | if (n) {
920 | n[1].appendChild(n[0]);
921 | delete nodeMap[ident];
922 | }
923 | });
924 | },
925 |
926 | serialize: function(element) {
927 | element = $(element);
928 | var options = Object.extend(Sortable.options(element), arguments[1] || { });
929 | var name = encodeURIComponent(
930 | (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
931 |
932 | if (options.tree) {
933 | return Sortable.tree(element, arguments[1]).children.map( function (item) {
934 | return [name + Sortable._constructIndex(item) + "[id]=" +
935 | encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
936 | }).flatten().join('&');
937 | } else {
938 | return Sortable.sequence(element, arguments[1]).map( function(item) {
939 | return name + "[]=" + encodeURIComponent(item);
940 | }).join('&');
941 | }
942 | }
943 | }
944 |
945 | // Returns true if child is contained within element
946 | Element.isParent = function(child, element) {
947 | if (!child.parentNode || child == element) return false;
948 | if (child.parentNode == element) return true;
949 | return Element.isParent(child.parentNode, element);
950 | }
951 |
952 | Element.findChildren = function(element, only, recursive, tagName) {
953 | if(!element.hasChildNodes()) return null;
954 | tagName = tagName.toUpperCase();
955 | if(only) only = [only].flatten();
956 | var elements = [];
957 | $A(element.childNodes).each( function(e) {
958 | if(e.tagName && e.tagName.toUpperCase()==tagName &&
959 | (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
960 | elements.push(e);
961 | if(recursive) {
962 | var grandchildren = Element.findChildren(e, only, recursive, tagName);
963 | if(grandchildren) elements.push(grandchildren);
964 | }
965 | });
966 |
967 | return (elements.length>0 ? elements.flatten() : []);
968 | }
969 |
970 | Element.offsetSize = function (element, type) {
971 | return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
972 | }
973 |
--------------------------------------------------------------------------------