active_admin-sortable_tree 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +10 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +163 -0
  7. data/Rakefile +57 -0
  8. data/active_admin-sortable_tree.gemspec +30 -0
  9. data/app/assets/javascripts/active_admin/sortable.js.coffee +91 -0
  10. data/app/assets/stylesheets/active_admin/sortable.css.sass +79 -0
  11. data/bin/rails +11 -0
  12. data/gemfiles/3.2.gemfile +7 -0
  13. data/gemfiles/4.0.gemfile +10 -0
  14. data/lib/active_admin/sortable_tree.rb +4 -0
  15. data/lib/active_admin/sortable_tree/controller_actions.rb +52 -0
  16. data/lib/active_admin/sortable_tree/engine.rb +19 -0
  17. data/lib/active_admin/sortable_tree/version.rb +5 -0
  18. data/lib/active_admin/views/index_as_block_decorator.rb +30 -0
  19. data/lib/active_admin/views/index_as_sortable.rb +154 -0
  20. data/lib/activeadmin-sortable-tree.rb +1 -0
  21. data/script/rails +11 -0
  22. data/spec/dummy/README.rdoc +261 -0
  23. data/spec/dummy/Rakefile +7 -0
  24. data/spec/dummy/app/admin/category.rb +15 -0
  25. data/spec/dummy/app/admin/category_tree.rb +9 -0
  26. data/spec/dummy/app/admin/dashboard.rb +5 -0
  27. data/spec/dummy/app/assets/javascripts/active_admin.js +2 -0
  28. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  29. data/spec/dummy/app/assets/javascripts/jquery.simulate.js +314 -0
  30. data/spec/dummy/app/assets/stylesheets/active_admin.css.scss +17 -0
  31. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  32. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  33. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  34. data/spec/dummy/app/mailers/.gitkeep +0 -0
  35. data/spec/dummy/app/models/.gitkeep +0 -0
  36. data/spec/dummy/app/models/admin_user.rb +6 -0
  37. data/spec/dummy/app/models/category.rb +4 -0
  38. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  39. data/spec/dummy/config.ru +4 -0
  40. data/spec/dummy/config/application.rb +56 -0
  41. data/spec/dummy/config/boot.rb +10 -0
  42. data/spec/dummy/config/database.yml +25 -0
  43. data/spec/dummy/config/environment.rb +5 -0
  44. data/spec/dummy/config/environments/development.rb +28 -0
  45. data/spec/dummy/config/environments/production.rb +64 -0
  46. data/spec/dummy/config/environments/test.rb +32 -0
  47. data/spec/dummy/config/initializers/active_admin.rb +231 -0
  48. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/spec/dummy/config/initializers/devise.rb +256 -0
  50. data/spec/dummy/config/initializers/inflections.rb +15 -0
  51. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  52. data/spec/dummy/config/initializers/secret_token.rb +8 -0
  53. data/spec/dummy/config/initializers/session_store.rb +8 -0
  54. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  55. data/spec/dummy/config/locales/devise.en.yml +59 -0
  56. data/spec/dummy/config/locales/en.yml +5 -0
  57. data/spec/dummy/config/routes.rb +61 -0
  58. data/spec/dummy/db/migrate/20140806024135_devise_create_admin_users.rb +48 -0
  59. data/spec/dummy/db/migrate/20140806024139_create_active_admin_comments.rb +19 -0
  60. data/spec/dummy/db/migrate/20140806032156_create_categories.rb +12 -0
  61. data/spec/dummy/db/schema.rb +58 -0
  62. data/spec/dummy/lib/assets/.gitkeep +0 -0
  63. data/spec/dummy/log/.gitkeep +0 -0
  64. data/spec/dummy/public/404.html +26 -0
  65. data/spec/dummy/public/422.html +26 -0
  66. data/spec/dummy/public/500.html +25 -0
  67. data/spec/dummy/public/favicon.ico +0 -0
  68. data/spec/dummy/script/rails +6 -0
  69. data/spec/features/sortable_spec.rb +58 -0
  70. data/spec/rails_helper.rb +65 -0
  71. data/spec/spec_helper.rb +58 -0
  72. data/spec/support/wait_for_ajax.rb +19 -0
  73. data/vendor/assets/javascripts/jquery.mjs.nestedSortable.js +613 -0
  74. metadata +231 -0
@@ -0,0 +1,48 @@
1
+ class DeviseCreateAdminUsers < ActiveRecord::Migration
2
+ def migrate(direction)
3
+ super
4
+ # Create a default user
5
+ AdminUser.create!(:email => 'admin@example.com', :password => 'password', :password_confirmation => 'password') if direction == :up
6
+ end
7
+
8
+ def change
9
+ create_table(:admin_users) do |t|
10
+ ## Database authenticatable
11
+ t.string :email, null: false, default: ""
12
+ t.string :encrypted_password, null: false, default: ""
13
+
14
+ ## Recoverable
15
+ t.string :reset_password_token
16
+ t.datetime :reset_password_sent_at
17
+
18
+ ## Rememberable
19
+ t.datetime :remember_created_at
20
+
21
+ ## Trackable
22
+ t.integer :sign_in_count, default: 0, null: false
23
+ t.datetime :current_sign_in_at
24
+ t.datetime :last_sign_in_at
25
+ t.string :current_sign_in_ip
26
+ t.string :last_sign_in_ip
27
+
28
+ ## Confirmable
29
+ # t.string :confirmation_token
30
+ # t.datetime :confirmed_at
31
+ # t.datetime :confirmation_sent_at
32
+ # t.string :unconfirmed_email # Only if using reconfirmable
33
+
34
+ ## Lockable
35
+ # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
36
+ # t.string :unlock_token # Only if unlock strategy is :email or :both
37
+ # t.datetime :locked_at
38
+
39
+
40
+ t.timestamps
41
+ end
42
+
43
+ add_index :admin_users, :email, unique: true
44
+ add_index :admin_users, :reset_password_token, unique: true
45
+ # add_index :admin_users, :confirmation_token, unique: true
46
+ # add_index :admin_users, :unlock_token, unique: true
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ class CreateActiveAdminComments < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :active_admin_comments do |t|
4
+ t.string :namespace
5
+ t.text :body
6
+ t.string :resource_id, :null => false
7
+ t.string :resource_type, :null => false
8
+ t.references :author, :polymorphic => true
9
+ t.timestamps
10
+ end
11
+ add_index :active_admin_comments, [:namespace]
12
+ add_index :active_admin_comments, [:author_type, :author_id]
13
+ add_index :active_admin_comments, [:resource_type, :resource_id]
14
+ end
15
+
16
+ def self.down
17
+ drop_table :active_admin_comments
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ class CreateCategories < ActiveRecord::Migration
2
+ def change
3
+ create_table :categories do |t|
4
+ t.string :name
5
+ t.string :ancestry
6
+ t.string :description
7
+ t.integer :position
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,58 @@
1
+ # encoding: UTF-8
2
+ # This file is auto-generated from the current state of the database. Instead
3
+ # of editing this file, please use the migrations feature of Active Record to
4
+ # incrementally modify your database, and then regenerate this schema definition.
5
+ #
6
+ # Note that this schema.rb definition is the authoritative source for your
7
+ # database schema. If you need to create the application database on another
8
+ # system, you should be using db:schema:load, not running all the migrations
9
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
11
+ #
12
+ # It's strongly recommended to check this file into your version control system.
13
+
14
+ ActiveRecord::Schema.define(:version => 20140806032156) do
15
+
16
+ create_table "active_admin_comments", :force => true do |t|
17
+ t.string "namespace"
18
+ t.text "body"
19
+ t.string "resource_id", :null => false
20
+ t.string "resource_type", :null => false
21
+ t.integer "author_id"
22
+ t.string "author_type"
23
+ t.datetime "created_at", :null => false
24
+ t.datetime "updated_at", :null => false
25
+ end
26
+
27
+ add_index "active_admin_comments", ["author_type", "author_id"], :name => "index_active_admin_comments_on_author_type_and_author_id"
28
+ add_index "active_admin_comments", ["namespace"], :name => "index_active_admin_comments_on_namespace"
29
+ add_index "active_admin_comments", ["resource_type", "resource_id"], :name => "index_active_admin_comments_on_resource_type_and_resource_id"
30
+
31
+ create_table "admin_users", :force => true do |t|
32
+ t.string "email", :default => "", :null => false
33
+ t.string "encrypted_password", :default => "", :null => false
34
+ t.string "reset_password_token"
35
+ t.datetime "reset_password_sent_at"
36
+ t.datetime "remember_created_at"
37
+ t.integer "sign_in_count", :default => 0, :null => false
38
+ t.datetime "current_sign_in_at"
39
+ t.datetime "last_sign_in_at"
40
+ t.string "current_sign_in_ip"
41
+ t.string "last_sign_in_ip"
42
+ t.datetime "created_at", :null => false
43
+ t.datetime "updated_at", :null => false
44
+ end
45
+
46
+ add_index "admin_users", ["email"], :name => "index_admin_users_on_email", :unique => true
47
+ add_index "admin_users", ["reset_password_token"], :name => "index_admin_users_on_reset_password_token", :unique => true
48
+
49
+ create_table "categories", :force => true do |t|
50
+ t.string "name"
51
+ t.string "ancestry"
52
+ t.string "description"
53
+ t.integer "position"
54
+ t.datetime "created_at", :null => false
55
+ t.datetime "updated_at", :null => false
56
+ end
57
+
58
+ end
File without changes
File without changes
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/404.html -->
21
+ <div class="dialog">
22
+ <h1>The page you were looking for doesn't exist.</h1>
23
+ <p>You may have mistyped the address or the page may have moved.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/422.html -->
21
+ <div class="dialog">
22
+ <h1>The change you wanted was rejected.</h1>
23
+ <p>Maybe you tried to change something you didn't have access to.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/500.html -->
21
+ <div class="dialog">
22
+ <h1>We're sorry, but something went wrong.</h1>
23
+ </div>
24
+ </body>
25
+ </html>
File without changes
@@ -0,0 +1,6 @@
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'
@@ -0,0 +1,58 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe "ActiveAdmin::SortableTree", type: :feature do
4
+ context "configured as sortable" do
5
+ it "sorts by dragging vertically", js: true do
6
+ bottom = Category.create! name: "bottom", position: 0
7
+ top = Category.create! name: "top", position: 1
8
+ middle = Category.create! name: "middle", position: 2
9
+
10
+ visit admin_categories_path
11
+
12
+ expect(all(".ui-sortable li h3").map(&:text)).to eq(["bottom", "top", "middle"])
13
+
14
+ wait_for_ajax { drag_element("#category_#{middle.id} h3", dy: -100) }
15
+ wait_for_ajax { drag_element("#category_#{top.id} h3", dy: -100) }
16
+
17
+ expect(all(".ui-sortable li h3").map(&:text)).to eq(["top", "middle", "bottom"])
18
+ expect(Category.order(:position).map(&:name)).to eq(["top", "middle", "bottom"])
19
+ end
20
+ end
21
+
22
+ context "configured as sortable tree" do
23
+ it "sorts by dragging vertically", js: true do
24
+ bottom = Category.create! name: "bottom", position: 0
25
+ top = Category.create! name: "top", position: 1
26
+ middle = Category.create! name: "middle", position: 2
27
+
28
+ visit admin_category_trees_path
29
+
30
+ expect(all(".ui-sortable li h3").map(&:text)).to eq(["bottom", "top", "middle"])
31
+
32
+ wait_for_ajax { drag_element("#category_tree_#{middle.id} h3", dy: -100) }
33
+ wait_for_ajax { drag_element("#category_tree_#{top.id} h3", dy: -100) }
34
+
35
+ expect(all(".ui-sortable li h3").map(&:text)).to eq(["top", "middle", "bottom"])
36
+ expect(Category.order(:position).map(&:name)).to eq(["top", "middle", "bottom"])
37
+ end
38
+
39
+ it "assigns hierarchy by dragging horizontally", js: true do
40
+ top = Category.create! name: "top", position: 0
41
+ middle = Category.create! name: "middle", position: 1
42
+ bottom = Category.create! name: "bottom", position: 2
43
+ expect(top.children).not_to include(middle)
44
+
45
+ visit admin_category_trees_path
46
+
47
+ wait_for_ajax { drag_element("#category_tree_#{middle.id} h3", dx: 40) }
48
+ wait_for_ajax { drag_element("#category_tree_#{bottom.id} h3", dx: 40) }
49
+
50
+ expect(top.children).to include(middle, bottom)
51
+ end
52
+ end
53
+
54
+ def drag_element(selector, options)
55
+ options.reverse_merge! moves: 20
56
+ page.execute_script(%Q($("#{selector}").simulate("drag", #{options.to_json} )))
57
+ end
58
+ end
@@ -0,0 +1,65 @@
1
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
2
+ ENV["RAILS_ENV"] ||= 'test'
3
+ require 'spec_helper'
4
+
5
+ require File.expand_path('../dummy/config/environment', __FILE__)
6
+
7
+ require 'rspec/rails'
8
+ require 'capybara/rails'
9
+ require 'phantomjs/poltergeist'
10
+ require 'database_cleaner'
11
+
12
+ Capybara.javascript_driver = :poltergeist
13
+
14
+ def reload_menus!
15
+ ActiveAdmin.application.namespaces.values.each{|n| n.reset_menu! }
16
+ end
17
+
18
+ def reload_routes!
19
+ Rails.application.reload_routes!
20
+ end
21
+
22
+ # Setup ActiveAdmin
23
+ ActiveAdmin.application.load_paths = [File.expand_path("../dummy/app/admin", __FILE__)]
24
+ ActiveAdmin.unload!
25
+ ActiveAdmin.load!
26
+ reload_menus!
27
+ reload_routes!
28
+
29
+ # Disabling authentication in specs so that we don't have to worry about
30
+ # it allover the place
31
+ ActiveAdmin.application.authentication_method = false
32
+ ActiveAdmin.application.current_user_method = false
33
+
34
+ Dir[File.expand_path("../support/**/*.rb", __FILE__)].each { |f| require f }
35
+
36
+ RSpec.configure do |config|
37
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
38
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
39
+
40
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
41
+ # examples within a transaction, remove the following line or assign false
42
+ # instead of true.
43
+ config.use_transactional_fixtures = false
44
+
45
+ config.before(:each) do
46
+ DatabaseCleaner.strategy = :transaction
47
+ end
48
+
49
+ config.before(:each, js: true) do
50
+ DatabaseCleaner.strategy = :truncation
51
+ end
52
+
53
+ config.before(:each) do
54
+ DatabaseCleaner.start
55
+ end
56
+
57
+ config.after(:each) do
58
+ DatabaseCleaner.clean
59
+ end
60
+
61
+ config.infer_spec_type_from_file_location!
62
+
63
+ config.include Devise::TestHelpers, type: :controller
64
+ config.include WaitForAjax, type: :feature
65
+ end
@@ -0,0 +1,58 @@
1
+ RSpec.configure do |config|
2
+ # These two settings work together to allow you to limit a spec run
3
+ # to individual examples or groups you care about by tagging them with
4
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
5
+ # get run.
6
+ config.filter_run :focus
7
+ config.run_all_when_everything_filtered = true
8
+
9
+ # Many RSpec users commonly either run the entire suite or an individual
10
+ # file, and it's useful to allow more verbose output when running an
11
+ # individual spec file.
12
+ if config.files_to_run.one?
13
+ # Use the documentation formatter for detailed output,
14
+ # unless a formatter has already been configured
15
+ # (e.g. via a command-line flag).
16
+ config.default_formatter = 'doc'
17
+ end
18
+
19
+ # Print the 10 slowest examples and example groups at the
20
+ # end of the spec run, to help surface which specs are running
21
+ # particularly slow.
22
+ config.profile_examples = 10
23
+
24
+ # Run specs in random order to surface order dependencies. If you find an
25
+ # order dependency and want to debug it, you can fix the order by providing
26
+ # the seed, which is printed after each run.
27
+ # --seed 1234
28
+ config.order = :random
29
+
30
+ # Seed global randomization in this process using the `--seed` CLI option.
31
+ # Setting this allows you to use `--seed` to deterministically reproduce
32
+ # test failures related to randomization by passing the same `--seed` value
33
+ # as the one that triggered the failure.
34
+ Kernel.srand config.seed
35
+
36
+ # rspec-expectations config goes here. You can use an alternate
37
+ # assertion/expectation library such as wrong or the stdlib/minitest
38
+ # assertions if you prefer.
39
+ config.expect_with :rspec do |expectations|
40
+ # Enable only the newer, non-monkey-patching expect syntax.
41
+ # For more details, see:
42
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
43
+ expectations.syntax = :expect
44
+ end
45
+
46
+ # rspec-mocks config goes here. You can use an alternate test double
47
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
48
+ config.mock_with :rspec do |mocks|
49
+ # Enable only the newer, non-monkey-patching expect syntax.
50
+ # For more details, see:
51
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
52
+ mocks.syntax = :expect
53
+
54
+ # Prevents you from mocking or stubbing a method that does not exist on
55
+ # a real object. This is generally recommended.
56
+ mocks.verify_partial_doubles = true
57
+ end
58
+ end
@@ -0,0 +1,19 @@
1
+ module WaitForAjax
2
+ def wait_for_ajax(count = 1)
3
+ page.execute_script 'window._ajaxCalls = 0, window._ajaxCompleteCounter = function() { window._ajaxCalls += 1; }'
4
+ page.execute_script '$(document).on("ajaxComplete", window._ajaxCompleteCounter)'
5
+
6
+ yield
7
+
8
+ Timeout.timeout(Capybara.default_wait_time) do
9
+ loop until finished_all_ajax_requests?(count)
10
+ end
11
+
12
+ page.execute_script '$(document).off("ajaxComplete", window._ajaxCompleteCounter)'
13
+ page.execute_script 'delete window._ajaxCompleteCounter, window._ajaxCalls'
14
+ end
15
+
16
+ def finished_all_ajax_requests?(count)
17
+ page.evaluate_script('window._ajaxCalls') == count
18
+ end
19
+ end
@@ -0,0 +1,613 @@
1
+ /*
2
+ * jQuery UI Nested Sortable
3
+ * v 2.0 / 29 oct 2012
4
+ * http://mjsarfatti.com/sandbox/nestedSortable
5
+ *
6
+ * Depends on:
7
+ * jquery.ui.sortable.js 1.10+
8
+ *
9
+ * Copyright (c) 2010-2013 Manuele J Sarfatti
10
+ * Licensed under the MIT License
11
+ * http://www.opensource.org/licenses/mit-license.php
12
+ */
13
+
14
+ (function($) {
15
+
16
+ function isOverAxis( x, reference, size ) {
17
+ return ( x > reference ) && ( x < ( reference + size ) );
18
+ }
19
+
20
+ $.widget("mjs.nestedSortable", $.extend({}, $.ui.sortable.prototype, {
21
+
22
+ options: {
23
+ doNotClear: false,
24
+ expandOnHover: 700,
25
+ isAllowed: function(placeholder, placeholderParent, originalItem) { return true; },
26
+ isTree: false,
27
+ listType: 'ol',
28
+ maxLevels: 0,
29
+ protectRoot: false,
30
+ rootID: null,
31
+ rtl: false,
32
+ startCollapsed: false,
33
+ tabSize: 20,
34
+
35
+ branchClass: 'mjs-nestedSortable-branch',
36
+ collapsedClass: 'mjs-nestedSortable-collapsed',
37
+ disableNestingClass: 'mjs-nestedSortable-no-nesting',
38
+ errorClass: 'mjs-nestedSortable-error',
39
+ expandedClass: 'mjs-nestedSortable-expanded',
40
+ hoveringClass: 'mjs-nestedSortable-hovering',
41
+ leafClass: 'mjs-nestedSortable-leaf'
42
+ },
43
+
44
+ _create: function() {
45
+ this.element.data('ui-sortable', this.element.data('mjs-nestedSortable'));
46
+
47
+ // mjs - prevent browser from freezing if the HTML is not correct
48
+ if (!this.element.is(this.options.listType))
49
+ throw new Error('nestedSortable: Please check that the listType option is set to your actual list type');
50
+
51
+ // mjs - force 'intersect' tolerance method if we have a tree with expanding/collapsing functionality
52
+ if (this.options.isTree && this.options.expandOnHover) {
53
+ this.options.tolerance = 'intersect';
54
+ }
55
+
56
+ $.ui.sortable.prototype._create.apply(this, arguments);
57
+
58
+ // mjs - prepare the tree by applying the right classes (the CSS is responsible for actual hide/show functionality)
59
+ if (this.options.isTree) {
60
+ var self = this;
61
+ $(this.items).each(function() {
62
+ var $li = this.item;
63
+ if ($li.children(self.options.listType).length) {
64
+ $li.addClass(self.options.branchClass);
65
+ // expand/collapse class only if they have children
66
+ if (self.options.startCollapsed) $li.addClass(self.options.collapsedClass);
67
+ else $li.addClass(self.options.expandedClass);
68
+ } else {
69
+ $li.addClass(self.options.leafClass);
70
+ }
71
+ })
72
+ }
73
+ },
74
+
75
+ _destroy: function() {
76
+ this.element
77
+ .removeData("mjs-nestedSortable")
78
+ .removeData("ui-sortable");
79
+ return $.ui.sortable.prototype._destroy.apply(this, arguments);
80
+ },
81
+
82
+ _mouseDrag: function(event) {
83
+ var i, item, itemElement, intersection,
84
+ o = this.options,
85
+ scrolled = false;
86
+
87
+ //Compute the helpers position
88
+ this.position = this._generatePosition(event);
89
+ this.positionAbs = this._convertPositionTo("absolute");
90
+
91
+ if (!this.lastPositionAbs) {
92
+ this.lastPositionAbs = this.positionAbs;
93
+ }
94
+
95
+ //Do scrolling
96
+ if(this.options.scroll) {
97
+ if(this.scrollParent[0] != document && this.scrollParent[0].tagName != 'HTML') {
98
+
99
+ if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) {
100
+ this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed;
101
+ } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) {
102
+ this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed;
103
+ }
104
+
105
+ if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) {
106
+ this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed;
107
+ } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) {
108
+ this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed;
109
+ }
110
+
111
+ } else {
112
+
113
+ if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) {
114
+ scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed);
115
+ } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) {
116
+ scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed);
117
+ }
118
+
119
+ if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) {
120
+ scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed);
121
+ } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) {
122
+ scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed);
123
+ }
124
+
125
+ }
126
+
127
+ if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour)
128
+ $.ui.ddmanager.prepareOffsets(this, event);
129
+ }
130
+
131
+ //Regenerate the absolute position used for position checks
132
+ this.positionAbs = this._convertPositionTo("absolute");
133
+
134
+ // mjs - find the top offset before rearrangement,
135
+ var previousTopOffset = this.placeholder.offset().top;
136
+
137
+ //Set the helper position
138
+ if(!this.options.axis || this.options.axis !== "y") {
139
+ this.helper[0].style.left = this.position.left+"px";
140
+ }
141
+ if(!this.options.axis || this.options.axis !== "x") {
142
+ this.helper[0].style.top = this.position.top+"px";
143
+ }
144
+
145
+ // mjs - check and reset hovering state at each cycle
146
+ this.hovering = this.hovering ? this.hovering : null;
147
+ this.mouseentered = this.mouseentered ? this.mouseentered : false;
148
+
149
+ // mjs - let's start caching some variables
150
+ var parentItem = (this.placeholder[0].parentNode.parentNode &&
151
+ $(this.placeholder[0].parentNode.parentNode).closest('.ui-sortable').length)
152
+ ? $(this.placeholder[0].parentNode.parentNode)
153
+ : null,
154
+ level = this._getLevel(this.placeholder),
155
+ childLevels = this._getChildLevels(this.helper);
156
+
157
+ var newList = document.createElement(o.listType);
158
+
159
+ //Rearrange
160
+ for (i = this.items.length - 1; i >= 0; i--) {
161
+
162
+ //Cache variables and intersection, continue if no intersection
163
+ item = this.items[i];
164
+ itemElement = item.item[0];
165
+ intersection = this._intersectsWithPointer(item);
166
+ if (!intersection) {
167
+ continue;
168
+ }
169
+
170
+ // Only put the placeholder inside the current Container, skip all
171
+ // items form other containers. This works because when moving
172
+ // an item from one container to another the
173
+ // currentContainer is switched before the placeholder is moved.
174
+ //
175
+ // Without this moving items in "sub-sortables" can cause the placeholder to jitter
176
+ // beetween the outer and inner container.
177
+ if (item.instance !== this.currentContainer) {
178
+ continue;
179
+ }
180
+
181
+ // cannot intersect with itself
182
+ // no useless actions that have been done before
183
+ // no action if the item moved is the parent of the item checked
184
+ if (itemElement !== this.currentItem[0] &&
185
+ this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement &&
186
+ !$.contains(this.placeholder[0], itemElement) &&
187
+ (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true)
188
+ ) {
189
+
190
+ // mjs - we are intersecting an element: trigger the mouseenter event and store this state
191
+ if (!this.mouseentered) {
192
+ $(itemElement).mouseenter();
193
+ this.mouseentered = true;
194
+ }
195
+
196
+ // mjs - if the element has children and they are hidden, show them after a delay (CSS responsible)
197
+ if (o.isTree && $(itemElement).hasClass(o.collapsedClass) && o.expandOnHover) {
198
+ if (!this.hovering) {
199
+ $(itemElement).addClass(o.hoveringClass);
200
+ var self = this;
201
+ this.hovering = window.setTimeout(function() {
202
+ $(itemElement).removeClass(o.collapsedClass).addClass(o.expandedClass);
203
+ self.refreshPositions();
204
+ self._trigger("expand", event, self._uiHash());
205
+ }, o.expandOnHover);
206
+ }
207
+ }
208
+
209
+ this.direction = intersection == 1 ? "down" : "up";
210
+
211
+ // mjs - rearrange the elements and reset timeouts and hovering state
212
+ if (this.options.tolerance == "pointer" || this._intersectsWithSides(item)) {
213
+ $(itemElement).mouseleave();
214
+ this.mouseentered = false;
215
+ $(itemElement).removeClass(o.hoveringClass);
216
+ this.hovering && window.clearTimeout(this.hovering);
217
+ this.hovering = null;
218
+
219
+ // mjs - do not switch container if it's a root item and 'protectRoot' is true
220
+ // or if it's not a root item but we are trying to make it root
221
+ if (o.protectRoot
222
+ && ! (this.currentItem[0].parentNode == this.element[0] // it's a root item
223
+ && itemElement.parentNode != this.element[0]) // it's intersecting a non-root item
224
+ ) {
225
+ if (this.currentItem[0].parentNode != this.element[0]
226
+ && itemElement.parentNode == this.element[0]
227
+ ) {
228
+
229
+ if ( ! $(itemElement).children(o.listType).length) {
230
+ itemElement.appendChild(newList);
231
+ o.isTree && $(itemElement).removeClass(o.leafClass).addClass(o.branchClass + ' ' + o.expandedClass);
232
+ }
233
+
234
+ var a = this.direction === "down" ? $(itemElement).prev().children(o.listType) : $(itemElement).children(o.listType);
235
+ if (a[0] !== undefined) {
236
+ this._rearrange(event, null, a);
237
+ }
238
+
239
+ } else {
240
+ this._rearrange(event, item);
241
+ }
242
+ } else if ( ! o.protectRoot) {
243
+ this._rearrange(event, item);
244
+ }
245
+ } else {
246
+ break;
247
+ }
248
+
249
+ // Clear emtpy ul's/ol's
250
+ this._clearEmpty(itemElement);
251
+
252
+ this._trigger("change", event, this._uiHash());
253
+ break;
254
+ }
255
+ }
256
+
257
+ // mjs - to find the previous sibling in the list, keep backtracking until we hit a valid list item.
258
+ var previousItem = this.placeholder[0].previousSibling ? $(this.placeholder[0].previousSibling) : null;
259
+ if (previousItem != null) {
260
+ while (previousItem[0].nodeName.toLowerCase() != 'li' || previousItem[0] == this.currentItem[0] || previousItem[0] == this.helper[0]) {
261
+ if (previousItem[0].previousSibling) {
262
+ previousItem = $(previousItem[0].previousSibling);
263
+ } else {
264
+ previousItem = null;
265
+ break;
266
+ }
267
+ }
268
+ }
269
+
270
+ // mjs - to find the next sibling in the list, keep stepping forward until we hit a valid list item.
271
+ var nextItem = this.placeholder[0].nextSibling ? $(this.placeholder[0].nextSibling) : null;
272
+ if (nextItem != null) {
273
+ while (nextItem[0].nodeName.toLowerCase() != 'li' || nextItem[0] == this.currentItem[0] || nextItem[0] == this.helper[0]) {
274
+ if (nextItem[0].nextSibling) {
275
+ nextItem = $(nextItem[0].nextSibling);
276
+ } else {
277
+ nextItem = null;
278
+ break;
279
+ }
280
+ }
281
+ }
282
+
283
+ this.beyondMaxLevels = 0;
284
+
285
+ // mjs - if the item is moved to the left, send it one level up but only if it's at the bottom of the list
286
+ if (parentItem != null
287
+ && nextItem == null
288
+ && ! (o.protectRoot && parentItem[0].parentNode == this.element[0])
289
+ &&
290
+ (o.rtl && (this.positionAbs.left + this.helper.outerWidth() > parentItem.offset().left + parentItem.outerWidth())
291
+ || ! o.rtl && (this.positionAbs.left < parentItem.offset().left))
292
+ ) {
293
+
294
+ parentItem.after(this.placeholder[0]);
295
+ if (o.isTree && parentItem.children(o.listItem).children('li:visible:not(.ui-sortable-helper)').length < 1) {
296
+ parentItem.removeClass(this.options.branchClass + ' ' + this.options.expandedClass)
297
+ .addClass(this.options.leafClass);
298
+ }
299
+ this._clearEmpty(parentItem[0]);
300
+ this._trigger("change", event, this._uiHash());
301
+ }
302
+ // mjs - if the item is below a sibling and is moved to the right, make it a child of that sibling
303
+ else if (previousItem != null
304
+ && ! previousItem.hasClass(o.disableNestingClass)
305
+ &&
306
+ (previousItem.children(o.listType).length && previousItem.children(o.listType).is(':visible')
307
+ || ! previousItem.children(o.listType).length)
308
+ && ! (o.protectRoot && this.currentItem[0].parentNode == this.element[0])
309
+ &&
310
+ (o.rtl && (this.positionAbs.left + this.helper.outerWidth() < previousItem.offset().left + previousItem.outerWidth() - o.tabSize)
311
+ || ! o.rtl && (this.positionAbs.left > previousItem.offset().left + o.tabSize))
312
+ ) {
313
+
314
+ this._isAllowed(previousItem, level, level+childLevels+1);
315
+
316
+ if (!previousItem.children(o.listType).length) {
317
+ previousItem[0].appendChild(newList);
318
+ o.isTree && previousItem.removeClass(o.leafClass).addClass(o.branchClass + ' ' + o.expandedClass);
319
+ }
320
+
321
+ // mjs - if this item is being moved from the top, add it to the top of the list.
322
+ if (previousTopOffset && (previousTopOffset <= previousItem.offset().top)) {
323
+ previousItem.children(o.listType).prepend(this.placeholder);
324
+ }
325
+ // mjs - otherwise, add it to the bottom of the list.
326
+ else {
327
+ previousItem.children(o.listType)[0].appendChild(this.placeholder[0]);
328
+ }
329
+
330
+ this._trigger("change", event, this._uiHash());
331
+ }
332
+ else {
333
+ this._isAllowed(parentItem, level, level+childLevels);
334
+ }
335
+
336
+ //Post events to containers
337
+ this._contactContainers(event);
338
+
339
+ //Interconnect with droppables
340
+ if($.ui.ddmanager) {
341
+ $.ui.ddmanager.drag(this, event);
342
+ }
343
+
344
+ //Call callbacks
345
+ this._trigger('sort', event, this._uiHash());
346
+
347
+ this.lastPositionAbs = this.positionAbs;
348
+ return false;
349
+
350
+ },
351
+
352
+ _mouseStop: function(event, noPropagation) {
353
+
354
+ // mjs - if the item is in a position not allowed, send it back
355
+ if (this.beyondMaxLevels) {
356
+
357
+ this.placeholder.removeClass(this.options.errorClass);
358
+
359
+ if (this.domPosition.prev) {
360
+ $(this.domPosition.prev).after(this.placeholder);
361
+ } else {
362
+ $(this.domPosition.parent).prepend(this.placeholder);
363
+ }
364
+
365
+ this._trigger("revert", event, this._uiHash());
366
+
367
+ }
368
+
369
+
370
+ // mjs - clear the hovering timeout, just to be sure
371
+ $('.'+this.options.hoveringClass).mouseleave().removeClass(this.options.hoveringClass);
372
+ this.mouseentered = false;
373
+ this.hovering && window.clearTimeout(this.hovering);
374
+ this.hovering = null;
375
+
376
+ $.ui.sortable.prototype._mouseStop.apply(this, arguments);
377
+
378
+ },
379
+
380
+ // mjs - this function is slightly modified to make it easier to hover over a collapsed element and have it expand
381
+ _intersectsWithSides: function(item) {
382
+
383
+ var half = this.options.isTree ? .8 : .5;
384
+
385
+ var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height*half), item.height),
386
+ isOverTopHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top - (item.height*half), item.height),
387
+ isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width),
388
+ verticalDirection = this._getDragVerticalDirection(),
389
+ horizontalDirection = this._getDragHorizontalDirection();
390
+
391
+ if (this.floating && horizontalDirection) {
392
+ return ((horizontalDirection == "right" && isOverRightHalf) || (horizontalDirection == "left" && !isOverRightHalf));
393
+ } else {
394
+ return verticalDirection && ((verticalDirection == "down" && isOverBottomHalf) || (verticalDirection == "up" && isOverTopHalf));
395
+ }
396
+
397
+ },
398
+
399
+ _contactContainers: function(event) {
400
+
401
+ if (this.options.protectRoot && this.currentItem[0].parentNode == this.element[0] ) {
402
+ return;
403
+ }
404
+
405
+ $.ui.sortable.prototype._contactContainers.apply(this, arguments);
406
+
407
+ },
408
+
409
+ _clear: function(event, noPropagation) {
410
+
411
+ $.ui.sortable.prototype._clear.apply(this, arguments);
412
+
413
+ // mjs - clean last empty ul/ol
414
+ for (var i = this.items.length - 1; i >= 0; i--) {
415
+ var item = this.items[i].item[0];
416
+ this._clearEmpty(item);
417
+ }
418
+
419
+ },
420
+
421
+ serialize: function(options) {
422
+
423
+ var o = $.extend({}, this.options, options),
424
+ items = this._getItemsAsjQuery(o && o.connected),
425
+ str = [];
426
+
427
+ $(items).each(function() {
428
+ var res = ($(o.item || this).attr(o.attribute || 'id') || '')
429
+ .match(o.expression || (/(.+)[-=_](.+)/)),
430
+ pid = ($(o.item || this).parent(o.listType)
431
+ .parent(o.items)
432
+ .attr(o.attribute || 'id') || '')
433
+ .match(o.expression || (/(.+)[-=_](.+)/));
434
+
435
+ if (res) {
436
+ str.push(((o.key || res[1]) + '[' + (o.key && o.expression ? res[1] : res[2]) + ']')
437
+ + '='
438
+ + (pid ? (o.key && o.expression ? pid[1] : pid[2]) : o.rootID));
439
+ }
440
+ });
441
+
442
+ if(!str.length && o.key) {
443
+ str.push(o.key + '=');
444
+ }
445
+
446
+ return str.join('&');
447
+
448
+ },
449
+
450
+ toHierarchy: function(options) {
451
+
452
+ var o = $.extend({}, this.options, options),
453
+ sDepth = o.startDepthCount || 0,
454
+ ret = [];
455
+
456
+ $(this.element).children(o.items).each(function () {
457
+ var level = _recursiveItems(this);
458
+ ret.push(level);
459
+ });
460
+
461
+ return ret;
462
+
463
+ function _recursiveItems(item) {
464
+ var id = ($(item).attr(o.attribute || 'id') || '').match(o.expression || (/(.+)[-=_](.+)/));
465
+ if (id) {
466
+ var currentItem = {"id" : id[2]};
467
+ if ($(item).children(o.listType).children(o.items).length > 0) {
468
+ currentItem.children = [];
469
+ $(item).children(o.listType).children(o.items).each(function() {
470
+ var level = _recursiveItems(this);
471
+ currentItem.children.push(level);
472
+ });
473
+ }
474
+ return currentItem;
475
+ }
476
+ }
477
+ },
478
+
479
+ toArray: function(options) {
480
+
481
+ var o = $.extend({}, this.options, options),
482
+ sDepth = o.startDepthCount || 0,
483
+ ret = [],
484
+ left = 1;
485
+
486
+ if (!o.excludeRoot) {
487
+ ret.push({
488
+ "item_id": o.rootID,
489
+ "parent_id": null,
490
+ "depth": sDepth,
491
+ "left": left,
492
+ "right": ($(o.items, this.element).length + 1) * 2
493
+ });
494
+ left++
495
+ }
496
+
497
+ $(this.element).children(o.items).each(function () {
498
+ left = _recursiveArray(this, sDepth + 1, left);
499
+ });
500
+
501
+ ret = ret.sort(function(a,b){ return (a.left - b.left); });
502
+
503
+ return ret;
504
+
505
+ function _recursiveArray(item, depth, left) {
506
+
507
+ var right = left + 1,
508
+ id,
509
+ pid;
510
+
511
+ if ($(item).children(o.listType).children(o.items).length > 0) {
512
+ depth ++;
513
+ $(item).children(o.listType).children(o.items).each(function () {
514
+ right = _recursiveArray($(this), depth, right);
515
+ });
516
+ depth --;
517
+ }
518
+
519
+ id = ($(item).attr(o.attribute || 'id')).match(o.expression || (/(.+)[-=_](.+)/));
520
+
521
+ if (depth === sDepth + 1) {
522
+ pid = o.rootID;
523
+ } else {
524
+ var parentItem = ($(item).parent(o.listType)
525
+ .parent(o.items)
526
+ .attr(o.attribute || 'id'))
527
+ .match(o.expression || (/(.+)[-=_](.+)/));
528
+ pid = parentItem[2];
529
+ }
530
+
531
+ if (id) {
532
+ ret.push({"item_id": id[2], "parent_id": pid, "depth": depth, "left": left, "right": right});
533
+ }
534
+
535
+ left = right + 1;
536
+ return left;
537
+ }
538
+
539
+ },
540
+
541
+ _clearEmpty: function(item) {
542
+ var o = this.options;
543
+
544
+ var emptyList = $(item).children(o.listType);
545
+
546
+ if (emptyList.length && !emptyList.children().length && !o.doNotClear) {
547
+ o.isTree && $(item).removeClass(o.branchClass + ' ' + o.expandedClass).addClass(o.leafClass);
548
+ emptyList.remove();
549
+ } else if (o.isTree && emptyList.length && emptyList.children().length && emptyList.is(':visible')) {
550
+ $(item).removeClass(o.leafClass).addClass(o.branchClass + ' ' + o.expandedClass);
551
+ } else if (o.isTree && emptyList.length && emptyList.children().length && !emptyList.is(':visible')) {
552
+ $(item).removeClass(o.leafClass).addClass(o.branchClass + ' ' + o.collapsedClass);
553
+ }
554
+
555
+ },
556
+
557
+ _getLevel: function(item) {
558
+
559
+ var level = 1;
560
+
561
+ if (this.options.listType) {
562
+ var list = item.closest(this.options.listType);
563
+ while (list && list.length > 0 &&
564
+ !list.is('.ui-sortable')) {
565
+ level++;
566
+ list = list.parent().closest(this.options.listType);
567
+ }
568
+ }
569
+
570
+ return level;
571
+ },
572
+
573
+ _getChildLevels: function(parent, depth) {
574
+ var self = this,
575
+ o = this.options,
576
+ result = 0;
577
+ depth = depth || 0;
578
+
579
+ $(parent).children(o.listType).children(o.items).each(function (index, child) {
580
+ result = Math.max(self._getChildLevels(child, depth + 1), result);
581
+ });
582
+
583
+ return depth ? result + 1 : result;
584
+ },
585
+
586
+ _isAllowed: function(parentItem, level, levels) {
587
+ var o = this.options,
588
+ maxLevels = this.placeholder.closest('.ui-sortable').nestedSortable('option', 'maxLevels'); // this takes into account the maxLevels set to the recipient list
589
+
590
+ // mjs - is the root protected?
591
+ // mjs - are we nesting too deep?
592
+ if ( ! o.isAllowed(this.placeholder, parentItem, this.currentItem)) {
593
+ this.placeholder.addClass(o.errorClass);
594
+ if (maxLevels < levels && maxLevels != 0) {
595
+ this.beyondMaxLevels = levels - maxLevels;
596
+ } else {
597
+ this.beyondMaxLevels = 1;
598
+ }
599
+ } else {
600
+ if (maxLevels < levels && maxLevels != 0) {
601
+ this.placeholder.addClass(o.errorClass);
602
+ this.beyondMaxLevels = levels - maxLevels;
603
+ } else {
604
+ this.placeholder.removeClass(o.errorClass);
605
+ this.beyondMaxLevels = 0;
606
+ }
607
+ }
608
+ }
609
+
610
+ }));
611
+
612
+ $.mjs.nestedSortable.prototype.options = $.extend({}, $.ui.sortable.prototype.options, $.mjs.nestedSortable.prototype.options);
613
+ })(jQuery);