├── .gitignore ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── Rakefile ├── app ├── assets │ ├── images │ │ └── sortable_tree_rails │ │ │ └── .keep │ ├── javascripts │ │ └── sortable_tree_rails │ │ │ └── .keep │ └── stylesheets │ │ └── sortable_tree_rails │ │ └── .keep ├── controllers │ ├── .keep │ └── sortable_tree_controller.rb ├── helpers │ ├── .keep │ └── sortable_tree_helper.rb ├── mailers │ └── .keep ├── models │ └── .keep └── views │ ├── .keep │ └── sortable_tree │ └── _sortable_tree.html.haml ├── bin └── rails ├── config └── routes.rb ├── lib ├── assets │ ├── javascripts │ │ ├── jquery.mjs.nestedSortable.js │ │ └── sortable_tree.js │ └── stylesheets │ │ └── sortable_tree.scss ├── sortable_tree_rails.rb ├── sortable_tree_rails │ ├── engine.rb │ └── version.rb └── tasks │ └── sortable_tree_rails_tasks.rake ├── readme.md ├── sortable_tree_rails.gemspec └── spec └── dummy ├── Rakefile ├── app ├── assets │ ├── images │ │ └── .keep │ ├── javascripts │ │ └── application.js │ └── stylesheets │ │ └── application.scss ├── controllers │ ├── application_controller.rb │ ├── categories_controller.rb │ └── concerns │ │ └── .keep ├── helpers │ └── application_helper.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── category.rb │ └── concerns │ │ └── .keep └── views │ ├── categories │ ├── _controls.html.haml │ ├── index.html.haml │ └── manage.html.haml │ └── layouts │ └── application.html.erb ├── bin ├── bundle ├── rails ├── rake └── setup ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── session_store.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── routes.rb └── secrets.yml ├── db ├── migrate │ ├── 20160528172331_create_categories.rb │ └── 20160528172947_add_ancestry_to_categories.rb └── schema.rb ├── lib └── assets │ └── .keep ├── log └── .keep └── public ├── 404.html ├── 422.html ├── 500.html └── favicon.ico /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .bundle/ 3 | log/*.log 4 | pkg/ 5 | spec/dummy/db/*.sqlite3 6 | spec/dummy/db/*.sqlite3-journal 7 | spec/dummy/log/*.log 8 | spec/dummy/tmp/ 9 | spec/dummy/.sass-cache 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in sortable_tree_rails.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | ruby "2.4.2" 9 | 10 | gem 'rails', '5.1.3' 11 | 12 | 13 | gem 'haml-rails', '1.0.0' 14 | gem 'sass-rails', '~> 5.0.6' 15 | gem 'uglifier', '3.2.0' 16 | gem 'jquery-rails', '4.3.1' 17 | gem "jquery-ui-rails" 18 | gem 'font-awesome-rails', '~>4.7' 19 | 20 | gem 'ancestry' 21 | 22 | # Declare any dependencies that are still in development here instead of in 23 | # your gemspec. These might include edge Rails or gems from your path or 24 | # Git. Remember to move these dependencies to your gemspec before releasing 25 | # your gem to rubygems.org. 26 | 27 | # To use a debugger 28 | # gem 'byebug', group: [:development, :test] 29 | 30 | gem 'tzinfo-data' #, platforms: [:mingw, :mswin, :x64_mingw, :jruby] 31 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | sortable_tree_rails (0.0.8) 5 | rails (>= 4.2.7) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (5.1.3) 11 | actionpack (= 5.1.3) 12 | nio4r (~> 2.0) 13 | websocket-driver (~> 0.6.1) 14 | actionmailer (5.1.3) 15 | actionpack (= 5.1.3) 16 | actionview (= 5.1.3) 17 | activejob (= 5.1.3) 18 | mail (~> 2.5, >= 2.5.4) 19 | rails-dom-testing (~> 2.0) 20 | actionpack (5.1.3) 21 | actionview (= 5.1.3) 22 | activesupport (= 5.1.3) 23 | rack (~> 2.0) 24 | rack-test (~> 0.6.3) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 27 | actionview (5.1.3) 28 | activesupport (= 5.1.3) 29 | builder (~> 3.1) 30 | erubi (~> 1.4) 31 | rails-dom-testing (~> 2.0) 32 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 33 | activejob (5.1.3) 34 | activesupport (= 5.1.3) 35 | globalid (>= 0.3.6) 36 | activemodel (5.1.3) 37 | activesupport (= 5.1.3) 38 | activerecord (5.1.3) 39 | activemodel (= 5.1.3) 40 | activesupport (= 5.1.3) 41 | arel (~> 8.0) 42 | activesupport (5.1.3) 43 | concurrent-ruby (~> 1.0, >= 1.0.2) 44 | i18n (~> 0.7) 45 | minitest (~> 5.1) 46 | tzinfo (~> 1.1) 47 | ancestry (3.0.1) 48 | activerecord (>= 3.2.0) 49 | arel (8.0.0) 50 | builder (3.2.3) 51 | concurrent-ruby (1.0.5) 52 | crass (1.0.3) 53 | erubi (1.7.0) 54 | erubis (2.7.0) 55 | execjs (2.7.0) 56 | ffi (1.9.21) 57 | ffi (1.9.21-x64-mingw32) 58 | font-awesome-rails (4.7.0.3) 59 | railties (>= 3.2, < 5.2) 60 | globalid (0.4.1) 61 | activesupport (>= 4.2.0) 62 | haml (5.0.4) 63 | temple (>= 0.8.0) 64 | tilt 65 | haml-rails (1.0.0) 66 | actionpack (>= 4.0.1) 67 | activesupport (>= 4.0.1) 68 | haml (>= 4.0.6, < 6.0) 69 | html2haml (>= 1.0.1) 70 | railties (>= 4.0.1) 71 | html2haml (2.2.0) 72 | erubis (~> 2.7.0) 73 | haml (>= 4.0, < 6) 74 | nokogiri (>= 1.6.0) 75 | ruby_parser (~> 3.5) 76 | i18n (0.9.5) 77 | concurrent-ruby (~> 1.0) 78 | jquery-rails (4.3.1) 79 | rails-dom-testing (>= 1, < 3) 80 | railties (>= 4.2.0) 81 | thor (>= 0.14, < 2.0) 82 | jquery-ui-rails (6.0.1) 83 | railties (>= 3.2.16) 84 | loofah (2.2.0) 85 | crass (~> 1.0.2) 86 | nokogiri (>= 1.5.9) 87 | mail (2.7.0) 88 | mini_mime (>= 0.1.1) 89 | method_source (0.9.0) 90 | mini_mime (1.0.0) 91 | mini_portile2 (2.3.0) 92 | minitest (5.11.3) 93 | nio4r (2.2.0) 94 | nokogiri (1.8.2) 95 | mini_portile2 (~> 2.3.0) 96 | nokogiri (1.8.2-x64-mingw32) 97 | mini_portile2 (~> 2.3.0) 98 | rack (2.0.4) 99 | rack-test (0.6.3) 100 | rack (>= 1.0) 101 | rails (5.1.3) 102 | actioncable (= 5.1.3) 103 | actionmailer (= 5.1.3) 104 | actionpack (= 5.1.3) 105 | actionview (= 5.1.3) 106 | activejob (= 5.1.3) 107 | activemodel (= 5.1.3) 108 | activerecord (= 5.1.3) 109 | activesupport (= 5.1.3) 110 | bundler (>= 1.3.0) 111 | railties (= 5.1.3) 112 | sprockets-rails (>= 2.0.0) 113 | rails-dom-testing (2.0.3) 114 | activesupport (>= 4.2.0) 115 | nokogiri (>= 1.6) 116 | rails-html-sanitizer (1.0.3) 117 | loofah (~> 2.0) 118 | railties (5.1.3) 119 | actionpack (= 5.1.3) 120 | activesupport (= 5.1.3) 121 | method_source 122 | rake (>= 0.8.7) 123 | thor (>= 0.18.1, < 2.0) 124 | rake (12.3.0) 125 | rb-fsevent (0.10.2) 126 | rb-inotify (0.9.10) 127 | ffi (>= 0.5.0, < 2) 128 | ruby_parser (3.11.0) 129 | sexp_processor (~> 4.9) 130 | sass (3.5.5) 131 | sass-listen (~> 4.0.0) 132 | sass-listen (4.0.0) 133 | rb-fsevent (~> 0.9, >= 0.9.4) 134 | rb-inotify (~> 0.9, >= 0.9.7) 135 | sass-rails (5.0.7) 136 | railties (>= 4.0.0, < 6) 137 | sass (~> 3.1) 138 | sprockets (>= 2.8, < 4.0) 139 | sprockets-rails (>= 2.0, < 4.0) 140 | tilt (>= 1.1, < 3) 141 | sexp_processor (4.10.1) 142 | sprockets (3.7.1) 143 | concurrent-ruby (~> 1.0) 144 | rack (> 1, < 3) 145 | sprockets-rails (3.2.1) 146 | actionpack (>= 4.0) 147 | activesupport (>= 4.0) 148 | sprockets (>= 3.0.0) 149 | sqlite3 (1.3.13) 150 | sqlite3 (1.3.13-x64-mingw32) 151 | temple (0.8.0) 152 | thor (0.20.0) 153 | thread_safe (0.3.6) 154 | tilt (2.0.8) 155 | tzinfo (1.2.5) 156 | thread_safe (~> 0.1) 157 | tzinfo-data (1.2018.3) 158 | tzinfo (>= 1.0.0) 159 | uglifier (3.2.0) 160 | execjs (>= 0.3.0, < 3) 161 | websocket-driver (0.6.5) 162 | websocket-extensions (>= 0.1.0) 163 | websocket-extensions (0.1.3) 164 | 165 | PLATFORMS 166 | ruby 167 | x64-mingw32 168 | 169 | DEPENDENCIES 170 | ancestry 171 | font-awesome-rails (~> 4.7) 172 | haml-rails (= 1.0.0) 173 | jquery-rails (= 4.3.1) 174 | jquery-ui-rails 175 | rails (= 5.1.3) 176 | sass-rails (~> 5.0.6) 177 | sortable_tree_rails! 178 | sqlite3 179 | tzinfo-data 180 | uglifier (= 3.2.0) 181 | 182 | RUBY VERSION 183 | ruby 2.4.2p198 184 | 185 | BUNDLED WITH 186 | 1.16.0 187 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Max Ivak 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'SortableTreeRails' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('readme.md') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | 21 | load 'rails/tasks/statistics.rake' 22 | 23 | 24 | 25 | Bundler::GemHelper.install_tasks 26 | 27 | -------------------------------------------------------------------------------- /app/assets/images/sortable_tree_rails/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/app/assets/images/sortable_tree_rails/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/sortable_tree_rails/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/app/assets/javascripts/sortable_tree_rails/.keep -------------------------------------------------------------------------------- /app/assets/stylesheets/sortable_tree_rails/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/app/assets/stylesheets/sortable_tree_rails/.keep -------------------------------------------------------------------------------- /app/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/app/controllers/.keep -------------------------------------------------------------------------------- /app/controllers/sortable_tree_controller.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module SortableTreeController 4 | module Sort 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | helper SortableTreeHelper 9 | end 10 | 11 | module ClassMethods 12 | 13 | def sortable_tree(class_name, options = {}) 14 | define_method("sort") do 15 | resource_class = class_name.to_s.camelize.constantize 16 | 17 | # options 18 | #options[:tree] = true 19 | 20 | sorting_attribute = options.fetch(:sorting_attribute, 'pos') 21 | parent_method = options.fetch(:parent_method, 'parent') 22 | #options[:sorting_attribute] ||= 'pos' 23 | #options[:parent_method] ||= 'parent' 24 | 25 | records = params[:cat].to_unsafe_h.inject({}) do |res, (resource, parent_resource)| 26 | res[resource_class.find(resource)] = resource_class.find(parent_resource) rescue nil 27 | res 28 | end 29 | 30 | errors = [] 31 | ActiveRecord::Base.transaction do 32 | records.each_with_index do |(record, parent_record), position| 33 | 34 | if sorting_attribute 35 | record.send "#{sorting_attribute}=", position 36 | end 37 | 38 | if parent_method 39 | record.send "#{parent_method}=", parent_record 40 | end 41 | 42 | errors << {record.id => record.errors} if !record.save 43 | end 44 | end 45 | 46 | # 47 | if errors.empty? 48 | head 200 49 | else 50 | render json: errors, status: 422 51 | end 52 | 53 | 54 | end 55 | end 56 | end 57 | 58 | 59 | 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /app/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/app/helpers/.keep -------------------------------------------------------------------------------- /app/helpers/sortable_tree_helper.rb: -------------------------------------------------------------------------------- 1 | module SortableTreeHelper 2 | 3 | def render_sortable_tree(items, opts={}) 4 | render :partial=>'sortable_tree/sortable_tree', locals: {items: items, options: opts} 5 | end 6 | 7 | 8 | def sortable_tree_render_nested_groups(groups, opts={}) 9 | content_tag(:ol) do 10 | groups.map do |item, sub_groups| 11 | content_tag(:li, {id: "cat_#{item.id}"} ) do 12 | #(item.title + sortable_tree_render_nested_groups(sub_groups, opts)).html_safe 13 | 14 | s = content_tag(:div, {class: 'item'}) do 15 | ( 16 | #'
'\ 17 | '

'+sortable_tree_build_show(item, opts)+'

'\ 18 | '
'+sortable_tree_build_actions(item, opts)+'
' 19 | ).html_safe 20 | end 21 | (s + sortable_tree_render_nested_groups(sub_groups, opts)).html_safe 22 | end 23 | 24 | end.join.html_safe 25 | end 26 | end 27 | 28 | def sortable_tree_build_actions(item, opts={}) 29 | partial = opts[:controls_partial] || nil 30 | if partial 31 | return render :partial=>partial, locals: {item: item, options: opts} 32 | else 33 | return "" 34 | end 35 | end 36 | 37 | def sortable_tree_build_show(item, opts={}) 38 | partial = opts[:show_partial] || nil 39 | if partial 40 | return render :partial=>partial, locals: {item: item, options: opts} 41 | else 42 | title = item.send(opts[:name_method] || :name) || '' 43 | return title 44 | end 45 | end 46 | 47 | end -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/app/mailers/.keep -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/app/models/.keep -------------------------------------------------------------------------------- /app/views/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/app/views/.keep -------------------------------------------------------------------------------- /app/views/sortable_tree/_sortable_tree.html.haml: -------------------------------------------------------------------------------- 1 | .sortable_tree_wrapper 2 | %ol.sortable_tree{data: {'sortable-url' => options[:sort_url], 'max-levels'=> (options[:max_levels] || 5)} } 3 | - items.each do |root, children| 4 | %li{id: "cat_#{root.id}"} 5 | .item 6 | /.cell.left 7 | / %i.handle 8 | %h3.cell.left 9 | =root.send(options[:name_method] || :name) 10 | .cell.right.controls 11 | - if options[:controls_partial] 12 | = render options[:controls_partial], item: root 13 | 14 | = sortable_tree_render_nested_groups(children, options.merge({list_tag: :ol, element_class: 'item'})) 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby.exe 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/sortable_tree_rails/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | end 3 | -------------------------------------------------------------------------------- /lib/assets/javascripts/jquery.mjs.nestedSortable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery UI Nested Sortable 3 | * v 2.1a / 2016-02-04 4 | * https://github.com/ilikenwf/nestedSortable 5 | * 6 | * Depends on: 7 | * jquery.ui.sortable.js 1.10+ 8 | * 9 | * Copyright (c) 2010-2016 Manuele J Sarfatti and contributors 10 | * Licensed under the MIT License 11 | * http://www.opensource.org/licenses/mit-license.php 12 | */ 13 | (function( factory ) { 14 | "use strict"; 15 | 16 | if ( typeof define === "function" && define.amd ) { 17 | 18 | // AMD. Register as an anonymous module. 19 | define([ 20 | "jquery", 21 | "jquery-ui/sortable" 22 | ], factory ); 23 | } else { 24 | 25 | // Browser globals 26 | factory( window.jQuery ); 27 | } 28 | }(function($) { 29 | "use strict"; 30 | 31 | function isOverAxis( x, reference, size ) { 32 | return ( x > reference ) && ( x < ( reference + size ) ); 33 | } 34 | 35 | $.widget("mjs.nestedSortable", $.extend({}, $.ui.sortable.prototype, { 36 | 37 | options: { 38 | disableParentChange: false, 39 | doNotClear: false, 40 | expandOnHover: 700, 41 | isAllowed: function() { return true; }, 42 | isTree: false, 43 | listType: "ol", 44 | maxLevels: 0, 45 | protectRoot: false, 46 | rootID: null, 47 | rtl: false, 48 | startCollapsed: false, 49 | tabSize: 20, 50 | 51 | branchClass: "mjs-nestedSortable-branch", 52 | collapsedClass: "mjs-nestedSortable-collapsed", 53 | disableNestingClass: "mjs-nestedSortable-no-nesting", 54 | errorClass: "mjs-nestedSortable-error", 55 | expandedClass: "mjs-nestedSortable-expanded", 56 | hoveringClass: "mjs-nestedSortable-hovering", 57 | leafClass: "mjs-nestedSortable-leaf", 58 | disabledClass: "mjs-nestedSortable-disabled" 59 | }, 60 | 61 | _create: function() { 62 | var self = this, 63 | err; 64 | 65 | this.element.data("ui-sortable", this.element.data("mjs-nestedSortable")); 66 | 67 | // mjs - prevent browser from freezing if the HTML is not correct 68 | if (!this.element.is(this.options.listType)) { 69 | err = "nestedSortable: " + 70 | "Please check that the listType option is set to your actual list type"; 71 | 72 | throw new Error(err); 73 | } 74 | 75 | // if we have a tree with expanding/collapsing functionality, 76 | // force 'intersect' tolerance method 77 | if (this.options.isTree && this.options.expandOnHover) { 78 | this.options.tolerance = "intersect"; 79 | } 80 | 81 | $.ui.sortable.prototype._create.apply(this, arguments); 82 | 83 | // prepare the tree by applying the right classes 84 | // (the CSS is responsible for actual hide/show functionality) 85 | if (this.options.isTree) { 86 | $(this.items).each(function() { 87 | var $li = this.item, 88 | hasCollapsedClass = $li.hasClass(self.options.collapsedClass), 89 | hasExpandedClass = $li.hasClass(self.options.expandedClass); 90 | 91 | if ($li.children(self.options.listType).length) { 92 | $li.addClass(self.options.branchClass); 93 | // expand/collapse class only if they have children 94 | 95 | if ( !hasCollapsedClass && !hasExpandedClass ) { 96 | if (self.options.startCollapsed) { 97 | $li.addClass(self.options.collapsedClass); 98 | } else { 99 | $li.addClass(self.options.expandedClass); 100 | } 101 | } 102 | } else { 103 | $li.addClass(self.options.leafClass); 104 | } 105 | }); 106 | } 107 | }, 108 | 109 | _destroy: function() { 110 | this.element 111 | .removeData("mjs-nestedSortable") 112 | .removeData("ui-sortable"); 113 | return $.ui.sortable.prototype._destroy.apply(this, arguments); 114 | }, 115 | 116 | _mouseDrag: function(event) { 117 | var i, 118 | item, 119 | itemElement, 120 | intersection, 121 | self = this, 122 | o = this.options, 123 | scrolled = false, 124 | $document = $(document), 125 | previousTopOffset, 126 | parentItem, 127 | level, 128 | childLevels, 129 | itemAfter, 130 | itemBefore, 131 | newList, 132 | method, 133 | a, 134 | previousItem, 135 | nextItem, 136 | helperIsNotSibling; 137 | 138 | //Compute the helpers position 139 | this.position = this._generatePosition(event); 140 | this.positionAbs = this._convertPositionTo("absolute"); 141 | 142 | if (!this.lastPositionAbs) { 143 | this.lastPositionAbs = this.positionAbs; 144 | } 145 | 146 | //Do scrolling 147 | if (this.options.scroll) { 148 | if (this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { 149 | 150 | if ( 151 | ( 152 | this.overflowOffset.top + 153 | this.scrollParent[0].offsetHeight 154 | ) - 155 | event.pageY < 156 | o.scrollSensitivity 157 | ) { 158 | scrolled = this.scrollParent.scrollTop() + o.scrollSpeed; 159 | this.scrollParent.scrollTop(scrolled); 160 | } else if ( 161 | event.pageY - 162 | this.overflowOffset.top < 163 | o.scrollSensitivity 164 | ) { 165 | scrolled = this.scrollParent.scrollTop() - o.scrollSpeed; 166 | this.scrollParent.scrollTop(scrolled); 167 | } 168 | 169 | if ( 170 | ( 171 | this.overflowOffset.left + 172 | this.scrollParent[0].offsetWidth 173 | ) - 174 | event.pageX < 175 | o.scrollSensitivity 176 | ) { 177 | scrolled = this.scrollParent.scrollLeft() + o.scrollSpeed; 178 | this.scrollParent.scrollLeft(scrolled); 179 | } else if ( 180 | event.pageX - 181 | this.overflowOffset.left < 182 | o.scrollSensitivity 183 | ) { 184 | scrolled = this.scrollParent.scrollLeft() - o.scrollSpeed; 185 | this.scrollParent.scrollLeft(scrolled); 186 | } 187 | 188 | } else { 189 | 190 | if ( 191 | event.pageY - 192 | $document.scrollTop() < 193 | o.scrollSensitivity 194 | ) { 195 | scrolled = $document.scrollTop() - o.scrollSpeed; 196 | $document.scrollTop(scrolled); 197 | } else if ( 198 | $(window).height() - 199 | ( 200 | event.pageY - 201 | $document.scrollTop() 202 | ) < 203 | o.scrollSensitivity 204 | ) { 205 | scrolled = $document.scrollTop() + o.scrollSpeed; 206 | $document.scrollTop(scrolled); 207 | } 208 | 209 | if ( 210 | event.pageX - 211 | $document.scrollLeft() < 212 | o.scrollSensitivity 213 | ) { 214 | scrolled = $document.scrollLeft() - o.scrollSpeed; 215 | $document.scrollLeft(scrolled); 216 | } else if ( 217 | $(window).width() - 218 | ( 219 | event.pageX - 220 | $document.scrollLeft() 221 | ) < 222 | o.scrollSensitivity 223 | ) { 224 | scrolled = $document.scrollLeft() + o.scrollSpeed; 225 | $document.scrollLeft(scrolled); 226 | } 227 | 228 | } 229 | 230 | if (scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { 231 | $.ui.ddmanager.prepareOffsets(this, event); 232 | } 233 | } 234 | 235 | //Regenerate the absolute position used for position checks 236 | this.positionAbs = this._convertPositionTo("absolute"); 237 | 238 | // mjs - find the top offset before rearrangement, 239 | previousTopOffset = this.placeholder.offset().top; 240 | 241 | //Set the helper position 242 | if (!this.options.axis || this.options.axis !== "y") { 243 | this.helper[0].style.left = this.position.left + "px"; 244 | } 245 | if (!this.options.axis || this.options.axis !== "x") { 246 | this.helper[0].style.top = (this.position.top) + "px"; 247 | } 248 | 249 | // mjs - check and reset hovering state at each cycle 250 | this.hovering = this.hovering ? this.hovering : null; 251 | this.mouseentered = this.mouseentered ? this.mouseentered : false; 252 | 253 | // mjs - let's start caching some variables 254 | (function() { 255 | var _parentItem = this.placeholder.parent().parent(); 256 | if (_parentItem && _parentItem.closest(".ui-sortable").length) { 257 | parentItem = _parentItem; 258 | } 259 | }.call(this)); 260 | 261 | level = this._getLevel(this.placeholder); 262 | childLevels = this._getChildLevels(this.helper); 263 | newList = document.createElement(o.listType); 264 | 265 | //Rearrange 266 | for (i = this.items.length - 1; i >= 0; i--) { 267 | 268 | //Cache variables and intersection, continue if no intersection 269 | item = this.items[i]; 270 | itemElement = item.item[0]; 271 | intersection = this._intersectsWithPointer(item); 272 | if (!intersection) { 273 | continue; 274 | } 275 | 276 | // Only put the placeholder inside the current Container, skip all 277 | // items form other containers. This works because when moving 278 | // an item from one container to another the 279 | // currentContainer is switched before the placeholder is moved. 280 | // 281 | // Without this moving items in "sub-sortables" can cause the placeholder to jitter 282 | // beetween the outer and inner container. 283 | if (item.instance !== this.currentContainer) { 284 | continue; 285 | } 286 | 287 | // No action if intersected item is disabled 288 | // and the element above or below in the direction we're going is also disabled 289 | if (itemElement.className.indexOf(o.disabledClass) !== -1) { 290 | // Note: intersection hardcoded direction values from 291 | // jquery.ui.sortable.js:_intersectsWithPointer 292 | if (intersection === 2) { 293 | // Going down 294 | itemAfter = this.items[i + 1]; 295 | if (itemAfter && itemAfter.item.hasClass(o.disabledClass)) { 296 | continue; 297 | } 298 | 299 | } else if (intersection === 1) { 300 | // Going up 301 | itemBefore = this.items[i - 1]; 302 | if (itemBefore && itemBefore.item.hasClass(o.disabledClass)) { 303 | continue; 304 | } 305 | } 306 | } 307 | 308 | method = intersection === 1 ? "next" : "prev"; 309 | 310 | // cannot intersect with itself 311 | // no useless actions that have been done before 312 | // no action if the item moved is the parent of the item checked 313 | if (itemElement !== this.currentItem[0] && 314 | this.placeholder[method]()[0] !== itemElement && 315 | !$.contains(this.placeholder[0], itemElement) && 316 | ( 317 | this.options.type === "semi-dynamic" ? 318 | !$.contains(this.element[0], itemElement) : 319 | true 320 | ) 321 | ) { 322 | 323 | // mjs - we are intersecting an element: 324 | // trigger the mouseenter event and store this state 325 | if (!this.mouseentered) { 326 | $(itemElement).mouseenter(); 327 | this.mouseentered = true; 328 | } 329 | 330 | // mjs - if the element has children and they are hidden, 331 | // show them after a delay (CSS responsible) 332 | if (o.isTree && $(itemElement).hasClass(o.collapsedClass) && o.expandOnHover) { 333 | if (!this.hovering) { 334 | $(itemElement).addClass(o.hoveringClass); 335 | this.hovering = window.setTimeout(function() { 336 | $(itemElement) 337 | .removeClass(o.collapsedClass) 338 | .addClass(o.expandedClass); 339 | 340 | self.refreshPositions(); 341 | self._trigger("expand", event, self._uiHash()); 342 | }, o.expandOnHover); 343 | } 344 | } 345 | 346 | this.direction = intersection === 1 ? "down" : "up"; 347 | 348 | // mjs - rearrange the elements and reset timeouts and hovering state 349 | if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { 350 | $(itemElement).mouseleave(); 351 | this.mouseentered = false; 352 | $(itemElement).removeClass(o.hoveringClass); 353 | if (this.hovering) { 354 | window.clearTimeout(this.hovering); 355 | } 356 | this.hovering = null; 357 | 358 | // mjs - do not switch container if 359 | // it's a root item and 'protectRoot' is true 360 | // or if it's not a root item but we are trying to make it root 361 | if (o.protectRoot && 362 | !( 363 | this.currentItem[0].parentNode === this.element[0] && 364 | // it's a root item 365 | itemElement.parentNode !== this.element[0] 366 | // it's intersecting a non-root item 367 | ) 368 | ) { 369 | if (this.currentItem[0].parentNode !== this.element[0] && 370 | itemElement.parentNode === this.element[0] 371 | ) { 372 | 373 | if ( !$(itemElement).children(o.listType).length) { 374 | itemElement.appendChild(newList); 375 | if (o.isTree) { 376 | $(itemElement) 377 | .removeClass(o.leafClass) 378 | .addClass(o.branchClass + " " + o.expandedClass); 379 | } 380 | } 381 | 382 | if (this.direction === "down") { 383 | a = $(itemElement).prev().children(o.listType); 384 | } else { 385 | a = $(itemElement).children(o.listType); 386 | } 387 | 388 | if (a[0] !== undefined) { 389 | this._rearrange(event, null, a); 390 | } 391 | 392 | } else { 393 | this._rearrange(event, item); 394 | } 395 | } else if (!o.protectRoot) { 396 | this._rearrange(event, item); 397 | } 398 | } else { 399 | break; 400 | } 401 | 402 | // Clear emtpy ul's/ol's 403 | this._clearEmpty(itemElement); 404 | 405 | this._trigger("change", event, this._uiHash()); 406 | break; 407 | } 408 | } 409 | 410 | // mjs - to find the previous sibling in the list, 411 | // keep backtracking until we hit a valid list item. 412 | (function() { 413 | var _previousItem = this.placeholder.prev(); 414 | if (_previousItem.length) { 415 | previousItem = _previousItem; 416 | } else { 417 | previousItem = null; 418 | } 419 | }.call(this)); 420 | 421 | if (previousItem != null) { 422 | while ( 423 | previousItem[0].nodeName.toLowerCase() !== "li" || 424 | previousItem[0].className.indexOf(o.disabledClass) !== -1 || 425 | previousItem[0] === this.currentItem[0] || 426 | previousItem[0] === this.helper[0] 427 | ) { 428 | if (previousItem[0].previousSibling) { 429 | previousItem = $(previousItem[0].previousSibling); 430 | } else { 431 | previousItem = null; 432 | break; 433 | } 434 | } 435 | } 436 | 437 | // mjs - to find the next sibling in the list, 438 | // keep stepping forward until we hit a valid list item. 439 | (function() { 440 | var _nextItem = this.placeholder.next(); 441 | if (_nextItem.length) { 442 | nextItem = _nextItem; 443 | } else { 444 | nextItem = null; 445 | } 446 | }.call(this)); 447 | 448 | if (nextItem != null) { 449 | while ( 450 | nextItem[0].nodeName.toLowerCase() !== "li" || 451 | nextItem[0].className.indexOf(o.disabledClass) !== -1 || 452 | nextItem[0] === this.currentItem[0] || 453 | nextItem[0] === this.helper[0] 454 | ) { 455 | if (nextItem[0].nextSibling) { 456 | nextItem = $(nextItem[0].nextSibling); 457 | } else { 458 | nextItem = null; 459 | break; 460 | } 461 | } 462 | } 463 | 464 | this.beyondMaxLevels = 0; 465 | 466 | // mjs - if the item is moved to the left, send it one level up 467 | // but only if it's at the bottom of the list 468 | if (parentItem != null && 469 | nextItem == null && 470 | !(o.protectRoot && parentItem[0].parentNode == this.element[0]) && 471 | ( 472 | o.rtl && 473 | ( 474 | this.positionAbs.left + 475 | this.helper.outerWidth() > parentItem.offset().left + 476 | parentItem.outerWidth() 477 | ) || 478 | !o.rtl && (this.positionAbs.left < parentItem.offset().left) 479 | ) 480 | ) { 481 | 482 | parentItem.after(this.placeholder[0]); 483 | helperIsNotSibling = !parentItem 484 | .children(o.listItem) 485 | .children("li:visible:not(.ui-sortable-helper)") 486 | .length; 487 | if (o.isTree && helperIsNotSibling) { 488 | parentItem 489 | .removeClass(this.options.branchClass + " " + this.options.expandedClass) 490 | .addClass(this.options.leafClass); 491 | } 492 | if(typeof parentItem !== 'undefined') 493 | this._clearEmpty(parentItem[0]); 494 | this._trigger("change", event, this._uiHash()); 495 | // mjs - if the item is below a sibling and is moved to the right, 496 | // make it a child of that sibling 497 | } else if (previousItem != null && 498 | !previousItem.hasClass(o.disableNestingClass) && 499 | ( 500 | previousItem.children(o.listType).length && 501 | previousItem.children(o.listType).is(":visible") || 502 | !previousItem.children(o.listType).length 503 | ) && 504 | !(o.protectRoot && this.currentItem[0].parentNode === this.element[0]) && 505 | ( 506 | o.rtl && 507 | ( 508 | this.positionAbs.left + 509 | this.helper.outerWidth() < 510 | previousItem.offset().left + 511 | previousItem.outerWidth() - 512 | o.tabSize 513 | ) || 514 | !o.rtl && 515 | (this.positionAbs.left > previousItem.offset().left + o.tabSize) 516 | ) 517 | ) { 518 | 519 | this._isAllowed(previousItem, level, level + childLevels + 1); 520 | 521 | if (!previousItem.children(o.listType).length) { 522 | previousItem[0].appendChild(newList); 523 | if (o.isTree) { 524 | previousItem 525 | .removeClass(o.leafClass) 526 | .addClass(o.branchClass + " " + o.expandedClass); 527 | } 528 | } 529 | 530 | // mjs - if this item is being moved from the top, add it to the top of the list. 531 | if (previousTopOffset && (previousTopOffset <= previousItem.offset().top)) { 532 | previousItem.children(o.listType).prepend(this.placeholder); 533 | } else { 534 | // mjs - otherwise, add it to the bottom of the list. 535 | previousItem.children(o.listType)[0].appendChild(this.placeholder[0]); 536 | } 537 | if(typeof parentItem !== 'undefined') 538 | this._clearEmpty(parentItem[0]); 539 | this._trigger("change", event, this._uiHash()); 540 | } else { 541 | this._isAllowed(parentItem, level, level + childLevels); 542 | } 543 | 544 | //Post events to containers 545 | this._contactContainers(event); 546 | 547 | //Interconnect with droppables 548 | if ($.ui.ddmanager) { 549 | $.ui.ddmanager.drag(this, event); 550 | } 551 | 552 | //Call callbacks 553 | this._trigger("sort", event, this._uiHash()); 554 | 555 | this.lastPositionAbs = this.positionAbs; 556 | return false; 557 | 558 | }, 559 | 560 | _mouseStop: function(event) { 561 | // mjs - if the item is in a position not allowed, send it back 562 | if (this.beyondMaxLevels) { 563 | 564 | this.placeholder.removeClass(this.options.errorClass); 565 | 566 | if (this.domPosition.prev) { 567 | $(this.domPosition.prev).after(this.placeholder); 568 | } else { 569 | $(this.domPosition.parent).prepend(this.placeholder); 570 | } 571 | 572 | this._trigger("revert", event, this._uiHash()); 573 | 574 | } 575 | 576 | // mjs - clear the hovering timeout, just to be sure 577 | $("." + this.options.hoveringClass) 578 | .mouseleave() 579 | .removeClass(this.options.hoveringClass); 580 | 581 | this.mouseentered = false; 582 | if (this.hovering) { 583 | window.clearTimeout(this.hovering); 584 | } 585 | this.hovering = null; 586 | 587 | this._relocate_event = event; 588 | this._pid_current = $(this.domPosition.parent).parent().attr("id"); 589 | this._sort_current = this.domPosition.prev ? $(this.domPosition.prev).next().index() : 0; 590 | $.ui.sortable.prototype._mouseStop.apply(this, arguments); //asybnchronous execution, @see _clear for the relocate event. 591 | }, 592 | 593 | // mjs - this function is slightly modified 594 | // to make it easier to hover over a collapsed element and have it expand 595 | _intersectsWithSides: function(item) { 596 | 597 | var half = this.options.isTree ? .8 : .5, 598 | isOverBottomHalf = isOverAxis( 599 | this.positionAbs.top + this.offset.click.top, 600 | item.top + (item.height * half), 601 | item.height 602 | ), 603 | isOverTopHalf = isOverAxis( 604 | this.positionAbs.top + this.offset.click.top, 605 | item.top - (item.height * half), 606 | item.height 607 | ), 608 | isOverRightHalf = isOverAxis( 609 | this.positionAbs.left + this.offset.click.left, 610 | item.left + (item.width / 2), 611 | item.width 612 | ), 613 | verticalDirection = this._getDragVerticalDirection(), 614 | horizontalDirection = this._getDragHorizontalDirection(); 615 | 616 | if (this.floating && horizontalDirection) { 617 | return ( 618 | (horizontalDirection === "right" && isOverRightHalf) || 619 | (horizontalDirection === "left" && !isOverRightHalf) 620 | ); 621 | } else { 622 | return verticalDirection && ( 623 | (verticalDirection === "down" && isOverBottomHalf) || 624 | (verticalDirection === "up" && isOverTopHalf) 625 | ); 626 | } 627 | 628 | }, 629 | 630 | _contactContainers: function() { 631 | 632 | if (this.options.protectRoot && this.currentItem[0].parentNode === this.element[0] ) { 633 | return; 634 | } 635 | 636 | $.ui.sortable.prototype._contactContainers.apply(this, arguments); 637 | 638 | }, 639 | 640 | _clear: function() { 641 | var i, 642 | item; 643 | 644 | $.ui.sortable.prototype._clear.apply(this, arguments); 645 | 646 | //relocate event 647 | if (!(this._pid_current === this._uiHash().item.parent().parent().attr("id") && 648 | this._sort_current === this._uiHash().item.index())) { 649 | this._trigger("relocate", this._relocate_event, this._uiHash()); 650 | } 651 | 652 | // mjs - clean last empty ul/ol 653 | for (i = this.items.length - 1; i >= 0; i--) { 654 | item = this.items[i].item[0]; 655 | this._clearEmpty(item); 656 | } 657 | 658 | }, 659 | 660 | serialize: function(options) { 661 | 662 | var o = $.extend({}, this.options, options), 663 | items = this._getItemsAsjQuery(o && o.connected), 664 | str = []; 665 | 666 | $(items).each(function() { 667 | var res = ($(o.item || this).attr(o.attribute || "id") || "") 668 | .match(o.expression || (/(.+)[-=_](.+)/)), 669 | pid = ($(o.item || this).parent(o.listType) 670 | .parent(o.items) 671 | .attr(o.attribute || "id") || "") 672 | .match(o.expression || (/(.+)[-=_](.+)/)); 673 | 674 | if (res) { 675 | str.push( 676 | ( 677 | (o.key || res[1]) + 678 | "[" + 679 | (o.key && o.expression ? res[1] : res[2]) + "]" 680 | ) + 681 | "=" + 682 | (pid ? (o.key && o.expression ? pid[1] : pid[2]) : o.rootID)); 683 | } 684 | }); 685 | 686 | if (!str.length && o.key) { 687 | str.push(o.key + "="); 688 | } 689 | 690 | return str.join("&"); 691 | 692 | }, 693 | 694 | toHierarchy: function(options) { 695 | 696 | var o = $.extend({}, this.options, options), 697 | ret = []; 698 | 699 | $(this.element).children(o.items).each(function() { 700 | var level = _recursiveItems(this); 701 | ret.push(level); 702 | }); 703 | 704 | return ret; 705 | 706 | function _recursiveItems(item) { 707 | var id = ($(item).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[-=_](.+)/)), 708 | currentItem; 709 | 710 | var data = $(item).data(); 711 | if (data.nestedSortableItem) { 712 | delete data.nestedSortableItem; // Remove the nestedSortableItem object from the data 713 | } 714 | 715 | if (id) { 716 | currentItem = { 717 | "id": id[2] 718 | }; 719 | 720 | currentItem = $.extend({}, currentItem, data); // Combine the two objects 721 | 722 | if ($(item).children(o.listType).children(o.items).length > 0) { 723 | currentItem.children = []; 724 | $(item).children(o.listType).children(o.items).each(function() { 725 | var level = _recursiveItems(this); 726 | currentItem.children.push(level); 727 | }); 728 | } 729 | return currentItem; 730 | } 731 | } 732 | }, 733 | 734 | toArray: function(options) { 735 | 736 | var o = $.extend({}, this.options, options), 737 | sDepth = o.startDepthCount || 0, 738 | ret = [], 739 | left = 1; 740 | 741 | if (!o.excludeRoot) { 742 | ret.push({ 743 | "item_id": o.rootID, 744 | "parent_id": null, 745 | "depth": sDepth, 746 | "left": left, 747 | "right": ($(o.items, this.element).length + 1) * 2 748 | }); 749 | left++; 750 | } 751 | 752 | $(this.element).children(o.items).each(function() { 753 | left = _recursiveArray(this, sDepth, left); 754 | }); 755 | 756 | ret = ret.sort(function(a, b) { return (a.left - b.left); }); 757 | 758 | return ret; 759 | 760 | function _recursiveArray(item, depth, _left) { 761 | 762 | var right = _left + 1, 763 | id, 764 | pid, 765 | parentItem; 766 | 767 | if ($(item).children(o.listType).children(o.items).length > 0) { 768 | depth++; 769 | $(item).children(o.listType).children(o.items).each(function() { 770 | right = _recursiveArray($(this), depth, right); 771 | }); 772 | depth--; 773 | } 774 | 775 | id = ($(item).attr(o.attribute || "id")).match(o.expression || (/(.+)[-=_](.+)/)); 776 | 777 | if (depth === sDepth) { 778 | pid = o.rootID; 779 | } else { 780 | parentItem = ($(item).parent(o.listType) 781 | .parent(o.items) 782 | .attr(o.attribute || "id")) 783 | .match(o.expression || (/(.+)[-=_](.+)/)); 784 | pid = parentItem[2]; 785 | } 786 | 787 | if (id) { 788 | var data = $(item).children('div').data(); 789 | var itemObj = $.extend( data, { 790 | "id":id[2], 791 | "parent_id":pid, 792 | "depth":depth, 793 | "left":_left, 794 | "right":right 795 | } ); 796 | ret.push( itemObj ); 797 | } 798 | 799 | _left = right + 1; 800 | return _left; 801 | } 802 | 803 | }, 804 | 805 | _clearEmpty: function (item) { 806 | function replaceClass(elem, search, replace, swap) { 807 | if (swap) { 808 | search = [replace, replace = search][0]; 809 | } 810 | 811 | $(elem).removeClass(search).addClass(replace); 812 | } 813 | 814 | var o = this.options, 815 | childrenList = $(item).children(o.listType), 816 | hasChildren = childrenList.has('li').length; 817 | 818 | var doNotClear = 819 | o.doNotClear || 820 | hasChildren || 821 | o.protectRoot && $(item)[0] === this.element[0]; 822 | 823 | if (o.isTree) { 824 | replaceClass(item, o.branchClass, o.leafClass, doNotClear); 825 | } 826 | 827 | if (!doNotClear) { 828 | childrenList.parent().removeClass(o.expandedClass); 829 | childrenList.remove(); 830 | } 831 | }, 832 | 833 | _getLevel: function(item) { 834 | 835 | var level = 1, 836 | list; 837 | 838 | if (this.options.listType) { 839 | list = item.closest(this.options.listType); 840 | while (list && list.length > 0 && !list.is(".ui-sortable")) { 841 | level++; 842 | list = list.parent().closest(this.options.listType); 843 | } 844 | } 845 | 846 | return level; 847 | }, 848 | 849 | _getChildLevels: function(parent, depth) { 850 | var self = this, 851 | o = this.options, 852 | result = 0; 853 | depth = depth || 0; 854 | 855 | $(parent).children(o.listType).children(o.items).each(function(index, child) { 856 | result = Math.max(self._getChildLevels(child, depth + 1), result); 857 | }); 858 | 859 | return depth ? result + 1 : result; 860 | }, 861 | 862 | _isAllowed: function(parentItem, level, levels) { 863 | var o = this.options, 864 | // this takes into account the maxLevels set to the recipient list 865 | maxLevels = this 866 | .placeholder 867 | .closest(".ui-sortable") 868 | .nestedSortable("option", "maxLevels"), 869 | 870 | // Check if the parent has changed to prevent it, when o.disableParentChange is true 871 | oldParent = this.currentItem.parent().parent(), 872 | disabledByParentchange = o.disableParentChange && ( 873 | //From somewhere to somewhere else, except the root 874 | typeof parentItem !== 'undefined' && !oldParent.is(parentItem) || 875 | typeof parentItem === 'undefined' && oldParent.is("li") //From somewhere to the root 876 | ); 877 | // mjs - is the root protected? 878 | // mjs - are we nesting too deep? 879 | if ( 880 | disabledByParentchange || 881 | !o.isAllowed(this.placeholder, parentItem, this.currentItem) 882 | ) { 883 | this.placeholder.addClass(o.errorClass); 884 | if (maxLevels < levels && maxLevels !== 0) { 885 | this.beyondMaxLevels = levels - maxLevels; 886 | } else { 887 | this.beyondMaxLevels = 1; 888 | } 889 | } else { 890 | if (maxLevels < levels && maxLevels !== 0) { 891 | this.placeholder.addClass(o.errorClass); 892 | this.beyondMaxLevels = levels - maxLevels; 893 | } else { 894 | this.placeholder.removeClass(o.errorClass); 895 | this.beyondMaxLevels = 0; 896 | } 897 | } 898 | } 899 | 900 | })); 901 | 902 | $.mjs.nestedSortable.prototype.options = $.extend( 903 | {}, 904 | $.ui.sortable.prototype.options, 905 | $.mjs.nestedSortable.prototype.options 906 | ); 907 | })); 908 | -------------------------------------------------------------------------------- /lib/assets/javascripts/sortable_tree.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | 3 | $('.sortable_tree').each(function() { 4 | $(this).nestedSortable({ 5 | forceHelperSizeType: true, 6 | errorClass: 'cantdoit', 7 | disableNesting: 'cantdoit', 8 | handle: '> .item', 9 | helper: 'clone', 10 | listType: 'ol', 11 | items: 'li', 12 | opacity: 0.6, 13 | placeholder: 'placeholder', 14 | revert: 250, 15 | maxLevels: $(this).data('max-levels'), 16 | //maxLevels: #{options[:max_levels] || 5}, 17 | //tabSize: 20, 18 | // protectRoot: $(this).data('protect-root'), 19 | 20 | // prevent drag flickers 21 | tolerance: 'pointer', 22 | toleranceElement: '> div', 23 | isTree: true, 24 | startCollapsed: false, 25 | //startCollapsed: $(this).data("start-collapsed"), 26 | 27 | relocate: function(){ 28 | //$(this).nestedSortable("disable"); 29 | var data = $(this).nestedSortable("serialize"); 30 | var url = $(this).data("sortable-url"); 31 | 32 | // update on server 33 | $.ajax({ 34 | url: url, 35 | type: "post", 36 | data: data 37 | }).always(function(){ 38 | //$(this).nestedSortable("enable"); 39 | 40 | $(this).find('.item').each(function(index){ 41 | if (index % 2){ 42 | $(this).removeClass('odd').addClass('even'); 43 | }else{ 44 | $(this).removeClass('even').addClass('odd'); 45 | } 46 | }); 47 | 48 | }).done(function(data){ 49 | 50 | }).fail(function(jqXHR, textStatus){ 51 | 52 | }); 53 | 54 | 55 | //$(this).nestedSortable("enable"); 56 | } 57 | }); // nested tree 58 | }); 59 | 60 | 61 | }); // document.ready 62 | -------------------------------------------------------------------------------- /lib/assets/stylesheets/sortable_tree.scss: -------------------------------------------------------------------------------- 1 | $cOddRowBackground: #f4f5f5; 2 | $cRowBorder: #e8e8e8; 3 | $cRowSelected: #d9e4ec; 4 | $cRowError: rgb(255,87,87); 5 | 6 | .ui-sortable > *{ 7 | cursor: move; 8 | } 9 | 10 | 11 | .disclose{ 12 | cursor: pointer; 13 | width: 10px; 14 | display: none; 15 | } 16 | 17 | 18 | 19 | ol.sortable_tree{ 20 | margin: 16px 0px 16px 0px; 21 | padding-left: 0px; 22 | } 23 | 24 | .sortable_tree_wrapper { 25 | .left { 26 | float: left !important; 27 | } 28 | .right { 29 | float: right !important; 30 | } 31 | 32 | ol { 33 | list-style-type:none; 34 | 35 | li { 36 | cursor: default !important; 37 | 38 | &.placeholder { 39 | background: lighten($cOddRowBackground, 10%); 40 | border: 1px dashed $cRowSelected; 41 | //.box-sizing(border-box); 42 | -webkit-box-sizing: border-box; 43 | -moz-box-sizing: border-box; 44 | box-sizing: border-box; 45 | 46 | &.cantdoit { 47 | border: 1px dashed $cRowError; 48 | } 49 | } 50 | 51 | .item { 52 | width: 100%; 53 | border-top: 1px solid $cRowBorder; 54 | border-bottom: 1px solid $cRowBorder; 55 | 56 | //.clearfix; 57 | &::after { 58 | content: ""; 59 | display: table; 60 | clear: both; 61 | } 62 | 63 | &.even { 64 | background: white; 65 | } 66 | 67 | &.odd { 68 | background: $cOddRowBackground; 69 | } 70 | 71 | &:hover { 72 | background-color: $cRowSelected; 73 | cursor: move; 74 | } 75 | 76 | .cell { 77 | margin: 0; 78 | padding: 10px 12px 8px 12px; 79 | } 80 | 81 | h3.cell { 82 | font-size: 16px; 83 | line-height: 14px; 84 | color: black; 85 | } 86 | } 87 | } 88 | 89 | > li > ol { 90 | margin-left: 30px; 91 | } 92 | 93 | li.mjs-nestedSortable-collapsed > ol { 94 | display: none; 95 | } 96 | 97 | li.mjs-nestedSortable-branch > div > .disclose { 98 | display: block; 99 | float: left; 100 | padding: 10px 5px 8px 5px; 101 | } 102 | 103 | li.mjs-nestedSortable-collapsed > div > .disclose > span:before { 104 | content: '+ '; 105 | } 106 | 107 | li.mjs-nestedSortable-expanded > div > .disclose > span:before { 108 | content: '- '; 109 | } 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /lib/sortable_tree_rails.rb: -------------------------------------------------------------------------------- 1 | require "sortable_tree_rails/engine" 2 | 3 | module SortableTreeRails 4 | end 5 | -------------------------------------------------------------------------------- /lib/sortable_tree_rails/engine.rb: -------------------------------------------------------------------------------- 1 | module SortableTreeRails 2 | class Engine < ::Rails::Engine 3 | 4 | 5 | 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/sortable_tree_rails/version.rb: -------------------------------------------------------------------------------- 1 | module SortableTreeRails 2 | VERSION = "0.0.10" 3 | end 4 | -------------------------------------------------------------------------------- /lib/tasks/sortable_tree_rails_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :sortable_tree_rails do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # SortableTreeRails 2 | 3 | GUI for sortable tree to manage data organized in tree with ancestry gem. 4 | 5 | Works with Rails 5, Rails 4. 6 | 7 | 8 | # Overview 9 | 10 | The gem uses: 11 | * [jquery-sortable plugin](https://johnny.github.io/jquery-sortable/) - jQuery Sortable UI 12 | * [nestedSortable](https://github.com/ilikenwf/nestedSortable) - a jQuery plugin for nested lists which extends jQuery Sortable UI 13 | 14 | # Demo 15 | See demo app in the repository (spec/dummy). 16 | Run page with the tree: `http://localhost:3000/categories/manage`. 17 | 18 | 19 | # Usage 20 | 21 | 22 | 23 | ### Gemfile 24 | 25 | ``` 26 | gem 'haml-rails' 27 | 28 | gem 'jquery-rails' 29 | gem "jquery-ui-rails" 30 | 31 | gem 'ancestry' 32 | gem 'sortable_tree_rails' 33 | ``` 34 | 35 | * the gem uses HAML 36 | 37 | 38 | ### routes 39 | 40 | ``` 41 | # config/routes.rb 42 | 43 | resources :categories do 44 | collection do 45 | post :sort 46 | end 47 | end 48 | 49 | ``` 50 | 51 | This page (sort_categories_path) will be used by the gem to update data after drag&drop. 52 | 53 | ### model 54 | 55 | It assumes that your model has already fields in DB for ancestry. 56 | 57 | ``` 58 | # app/models/category.rb 59 | 60 | class Category < ActiveRecord::Base 61 | 62 | has_ancestry 63 | 64 | # it uses column ancestry_depth 65 | # has_ancestry :cache_depth=>true 66 | 67 | end 68 | 69 | ``` 70 | 71 | 72 | ### Javascript 73 | 74 | Include js files in your assets file `application.js`: 75 | 76 | ``` 77 | //= require jquery2 78 | //= require jquery-ui 79 | 80 | //=require jquery.mjs.nestedSortable.js 81 | //=require sortable_tree.js 82 | 83 | 84 | ``` 85 | 86 | it depends on jquery and jquery-ui which come from gems 'jquery-rails', 'jquery-ui-rails'. 87 | Change `application.js` to include jquery and jquery-ui (jquery-ui/sortable) if you are not using these gems. 88 | 89 | 90 | 91 | 92 | ### CSS 93 | 94 | Add CSS file to your styles. 95 | 96 | for SCSS (app/assets/application.scss): 97 | 98 | ``` 99 | ... your css here .. 100 | 101 | @import "sortable_tree"; 102 | 103 | 104 | ``` 105 | 106 | 107 | ### controller 108 | 109 | ``` 110 | class CategoriesController < ApplicationController 111 | include SortableTreeController::Sort 112 | sortable_tree 'Category', {parent_method: 'parent'} 113 | 114 | def manage 115 | # get items to show in tree 116 | @items = Category.all.arrange(:order => :pos) 117 | 118 | end 119 | 120 | end 121 | 122 | ``` 123 | 124 | ### view 125 | 126 | ``` 127 | # app/views/categories/manage.html.haml 128 | 129 | = render_sortable_tree(@items, {name_method: :name, sort_url: sort_categories_url, max_levels: 5, controls_partial: 'controls'}) 130 | 131 | ``` 132 | 133 | 134 | ``` 135 | # app/views/categories/_controls.html.haml 136 | 137 | = link_to 'Edit', edit_category_url(item) 138 | = link_to 'Delete', category_url(item), :method => :delete, :data => { :confirm => 'Are you sure?' } 139 | 140 | ``` 141 | 142 | 143 | 144 | # Customize 145 | 146 | ## Options 147 | 148 | ### Options for controller 149 | 150 | in controller: 151 | ``` 152 | 153 | include SortableTreeController::Sort 154 | sortable_tree 'ClassName', {_options_here_} 155 | 156 | ``` 157 | 158 | 159 | * ClassName - class name (camel case). For example, 'Category'. 160 | * :sorting_attribute - attribute used for sorting. Set to nil to disable sorting. 161 | * :parent_method - method used to access parent for the item. Set to nil to disable tree (not updating parent). 162 | 163 | Examples. 164 | 165 | * Example. For model with ancestry. 166 | If you use ancestry in model - set :parent_method to 'parent'. 167 | 168 | ``` 169 | include SortableTreeController::Sort 170 | sortable_tree 'ClassName', {parent_method: 'parent', sorting_attribute: 'pos'} 171 | ``` 172 | 173 | * Example. Do only sorting, without tree. 174 | 175 | ``` 176 | include SortableTreeController::Sort 177 | sortable_tree 'ClassName', {sorting_attribute: 'pos', parent_method: nil} 178 | ``` 179 | 180 | * Example. No sorting, update only parent. 181 | 182 | ``` 183 | include SortableTreeController::Sort 184 | sortable_tree 'ClassName', {sorting_attribute: nil, parent_method: 'parent'} 185 | ``` 186 | 187 | 188 | 189 | ## Options for view 190 | 191 | ``` 192 | = render_sortable_tree(@items, {__options_here}) 193 | ``` 194 | 195 | * :name_method - defined which model method (usually, a column name) will be used to show name (default: :name) 196 | * :sort_url - URL used to update data after item is moved to a new position 197 | * :max_levels - max levels to show in tree (default: 5) 198 | * :controls_partial - specify what partial view to use to show control links for each item in a tree. Set to nil to not show controls. 199 | 200 | 201 | 202 | * example. 203 | 204 | ``` 205 | = render_sortable_tree(@items, {name_method: :name, sort_url: sort_categories_url, max_levels: 5}) 206 | ``` 207 | 208 | 209 | * example of partial view with controls. 210 | 211 | * main view: 212 | ``` 213 | = render_sortable_tree(@items, {name_method: :name, sort_url: sort_categories_url, max_levels: 5, controls_partial: 'controls'}) 214 | ``` 215 | 216 | * partial with controls. Use local variable `item` in the partial view. 217 | 218 | `_controls.html.haml`: 219 | 220 | ``` 221 | = link_to 'Edit', edit_category_url(item) 222 | = link_to 'Delete', category_url(item), :method => :delete, :data => { :confirm => 'Are you sure?' } 223 | 224 | ``` 225 | 226 | 227 | # Customize 228 | 229 | ## customize view 230 | 231 | * edit file 'views/sortable/_sortable.html.haml' to access the whole layout 232 | 233 | 234 | 235 | 236 | 237 | # How it works 238 | 239 | read Wiki 240 | 241 | 242 | # Similar gems 243 | 244 | GUI for sortable tree with awesome_nested_set gem: 245 | * https://github.com/the-teacher/the_sortable_tree 246 | * https://github.com/winescout/the_sortable_tree 247 | 248 | 249 | # Credits 250 | * Some pieces of code was created by inspiration of gem [ActiveAdmin Sortable Tree](https://github.com/maxivak/activeadmin-sortable-tree/) 251 | 252 | 253 | -------------------------------------------------------------------------------- /sortable_tree_rails.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "sortable_tree_rails/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "sortable_tree_rails" 9 | s.version = SortableTreeRails::VERSION 10 | s.authors = ["Max Ivak"] 11 | s.email = ["maxivak@gmail.com"] 12 | s.homepage = "https://github.com/maxivak/sortable_tree_rails" 13 | s.summary = "GUI for sortable tree with ancestry gem" 14 | s.description = "GUI for sortable tree to manage data organized in tree with ancestry gem." 15 | s.license = "MIT" 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "readme.md"] 18 | 19 | s.add_dependency "rails", ">= 4.2.7" 20 | 21 | s.add_development_dependency "sqlite3" 22 | end 23 | -------------------------------------------------------------------------------- /spec/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 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/spec/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | 14 | //= require jquery2 15 | //= require jquery-ui 16 | 17 | //=require jquery.mjs.nestedSortable.js 18 | //=require sortable_tree.js 19 | 20 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | @import "sortable_tree"; -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/categories_controller.rb: -------------------------------------------------------------------------------- 1 | class CategoriesController < ApplicationController 2 | protect_from_forgery with: :null_session 3 | 4 | def index 5 | 6 | end 7 | 8 | include SortableTreeController::Sort 9 | sortable_tree 'Category', {parent_method: 'parent', sorting_attribute: 'pos'} 10 | #sortable_tree 'Category', {parent_method: nil, sorting_attribute: nil} 11 | 12 | def manage 13 | # fix ancestry 14 | #Category.build_ancestry_from_parent_ids! 15 | 16 | # 17 | @items = Category.all.arrange(:order => :pos) 18 | 19 | end 20 | 21 | 22 | end -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/spec/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/spec/dummy/app/mailers/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/spec/dummy/app/models/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/category.rb: -------------------------------------------------------------------------------- 1 | class Category < ActiveRecord::Base 2 | # 3 | has_ancestry :cache_depth=>true 4 | # it uses column ancestry_depth 5 | 6 | 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/spec/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/views/categories/_controls.html.haml: -------------------------------------------------------------------------------- 1 | = link_to 'Edit', edit_category_url(item) 2 | = link_to 'Delete', category_url(item), :method => :delete, :data => { :confirm => 'Are you sure?' } -------------------------------------------------------------------------------- /spec/dummy/app/views/categories/index.html.haml: -------------------------------------------------------------------------------- 1 | %h1 Categories 2 | 3 | %ul 4 | - @items.each do |item| 5 | %li 6 | =item.name 7 | -------------------------------------------------------------------------------- /spec/dummy/app/views/categories/manage.html.haml: -------------------------------------------------------------------------------- 1 | = render_sortable_tree(@items, {name_method: :name, sort_url: sort_categories_url, max_levels: 5, controls_partial: 'controls'}) 2 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby.exe 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby.exe 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby.exe 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby.exe 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /spec/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 Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "action_view/railtie" 8 | require "sprockets/railtie" 9 | # require "rails/test_unit/railtie" 10 | 11 | Bundler.require(*Rails.groups) 12 | require "sortable_tree_rails" 13 | 14 | module Dummy 15 | class Application < Rails::Application 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 21 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 22 | # config.time_zone = 'Central Time (US & Canada)' 23 | 24 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 25 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 26 | # config.i18n.default_locale = :de 27 | 28 | # Do not swallow errors in after_commit/after_rollback callbacks. 29 | #config.active_record.raise_in_transactional_callbacks = true 30 | end 31 | end 32 | 33 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.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 web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 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 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 35 | # yet still be able to expire them through the digest params. 36 | config.assets.digest = true 37 | 38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Use the lowest log level to ensure availability of diagnostic information 48 | # when problems arise. 49 | config.log_level = :debug 50 | 51 | # Prepend all log lines with the following tags. 52 | # config.log_tags = [ :subdomain, :uuid ] 53 | 54 | # Use a different logger for distributed setups. 55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 56 | 57 | # Use a different cache store in production. 58 | # config.cache_store = :mem_cache_store 59 | 60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 61 | # config.action_controller.asset_host = 'http://assets.example.com' 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Use default logging formatter so that PID and timestamp are not suppressed. 75 | config.log_formatter = ::Logger::Formatter.new 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | end 80 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.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 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.scss, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /spec/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 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/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. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/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 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # The priority is based upon order of creation: first created -> highest priority. 3 | # See how all your routes lay out with "rake routes". 4 | 5 | # You can have the root of your site routed with "root" 6 | # root 'welcome#index' 7 | 8 | resources :categories do 9 | collection do 10 | get :manage 11 | post :sort 12 | end 13 | 14 | end 15 | 16 | 17 | # Example of regular route: 18 | # get 'products/:id' => 'catalog#view' 19 | 20 | # Example of named route that can be invoked with purchase_url(id: product.id) 21 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 22 | 23 | # Example resource route (maps HTTP verbs to controller actions automatically): 24 | # resources :products 25 | 26 | # Example resource route with options: 27 | # resources :products do 28 | # member do 29 | # get 'short' 30 | # post 'toggle' 31 | # end 32 | # 33 | # collection do 34 | # get 'sold' 35 | # end 36 | # end 37 | 38 | # Example resource route with sub-resources: 39 | # resources :products do 40 | # resources :comments, :sales 41 | # resource :seller 42 | # end 43 | 44 | # Example resource route with more complex sub-resources: 45 | # resources :products do 46 | # resources :comments 47 | # resources :sales do 48 | # get 'recent', on: :collection 49 | # end 50 | # end 51 | 52 | # Example resource route with concerns: 53 | # concern :toggleable do 54 | # post 'toggle' 55 | # end 56 | # resources :posts, concerns: :toggleable 57 | # resources :photos, concerns: :toggleable 58 | 59 | # Example resource route within a namespace: 60 | # namespace :admin do 61 | # # Directs /admin/products/* to Admin::ProductsController 62 | # # (app/controllers/admin/products_controller.rb) 63 | # resources :products 64 | # end 65 | end 66 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 2a40c0cf27669b3b32e2e18355f927aac5af68be8dcb1f5b9b7b05f9c0c7494067993cf630e495a83cb3dd5b4f45be0d0d8b756da8c6f571f3aab081750409af 15 | 16 | test: 17 | secret_key_base: de88eebcb984b2d91aa7f12462904b7f83d918b8982493351e573444bc6be32ac888ff12fa443d992bd16fc2773482ef9c39ce3611e5e2580ac67e295f2b7173 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20160528172331_create_categories.rb: -------------------------------------------------------------------------------- 1 | class CreateCategories < ActiveRecord::Migration 2 | def change 3 | create_table :categories do |t| 4 | t.string :name 5 | t.text :description 6 | 7 | t.integer :pos 8 | t.integer :parent_id 9 | 10 | t.timestamps null: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20160528172947_add_ancestry_to_categories.rb: -------------------------------------------------------------------------------- 1 | class AddAncestryToCategories < ActiveRecord::Migration 2 | def change 3 | add_column :categories, :ancestry, :string 4 | add_column :categories, :ancestry_depth, :integer 5 | 6 | add_index :categories, :ancestry 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 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 that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20160528172947) do 15 | 16 | create_table "categories", force: :cascade do |t| 17 | t.string "name" 18 | t.text "description" 19 | t.integer "pos" 20 | t.datetime "created_at", null: false 21 | t.datetime "updated_at", null: false 22 | t.string "ancestry" 23 | t.integer "ancestry_depth" 24 | end 25 | 26 | add_index "categories", ["ancestry"], name: "index_categories_on_ancestry" 27 | 28 | end 29 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/spec/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/spec/dummy/log/.keep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxivak/sortable_tree_rails/949f1c8665bf7c393a191098e9cdf685ecba8a0f/spec/dummy/public/favicon.ico --------------------------------------------------------------------------------