active_admin-sortable_tree 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);