weixin_pam 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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +165 -0
- data/Rakefile +37 -0
- data/app/assets/javascripts/jquery-sortable.js +699 -0
- data/app/assets/javascripts/weixin_pam/application.js +30 -0
- data/app/assets/javascripts/weixin_pam/diymenus.js +90 -0
- data/app/assets/javascripts/weixin_pam/public_accounts.js +2 -0
- data/app/assets/javascripts/weixin_pam/user_accounts.js +23 -0
- data/app/assets/stylesheets/weixin_pam/application.scss +31 -0
- data/app/assets/stylesheets/weixin_pam/diymenus.scss +52 -0
- data/app/assets/stylesheets/weixin_pam/jquery-dragable.scss +26 -0
- data/app/assets/stylesheets/weixin_pam/public_accounts.css +4 -0
- data/app/assets/stylesheets/weixin_pam/user_accounts.css +4 -0
- data/app/controllers/weixin_pam/application_controller.rb +4 -0
- data/app/controllers/weixin_pam/diymenus_controller.rb +100 -0
- data/app/controllers/weixin_pam/public_accounts_controller.rb +62 -0
- data/app/controllers/weixin_pam/user_accounts_controller.rb +76 -0
- data/app/decorators/controllers/weixin_rails_middleware/weixin_controller_decorator.rb +31 -0
- data/app/helpers/weixin_pam/application_helper.rb +18 -0
- data/app/helpers/weixin_pam/diymenus_helper.rb +4 -0
- data/app/helpers/weixin_pam/public_accounts_helper.rb +4 -0
- data/app/helpers/weixin_pam/user_accounts_helper.rb +4 -0
- data/app/models/weixin_pam/diymenu.rb +58 -0
- data/app/models/weixin_pam/public_account.rb +87 -0
- data/app/models/weixin_pam/user_account.rb +67 -0
- data/app/views/layouts/weixin_pam/_nav_bar.html.erb +37 -0
- data/app/views/layouts/weixin_pam/application.html.erb +26 -0
- data/app/views/weixin_pam/diymenus/_form.html.erb +18 -0
- data/app/views/weixin_pam/diymenus/edit.html.erb +3 -0
- data/app/views/weixin_pam/diymenus/index.html.slim +32 -0
- data/app/views/weixin_pam/diymenus/new.html.erb +3 -0
- data/app/views/weixin_pam/public_accounts/_form.html.erb +12 -0
- data/app/views/weixin_pam/public_accounts/edit.html.erb +3 -0
- data/app/views/weixin_pam/public_accounts/index.html.erb +30 -0
- data/app/views/weixin_pam/public_accounts/new.html.erb +3 -0
- data/app/views/weixin_pam/public_accounts/show.html.erb +17 -0
- data/app/views/weixin_pam/user_accounts/_form.html.erb +37 -0
- data/app/views/weixin_pam/user_accounts/edit.html.erb +6 -0
- data/app/views/weixin_pam/user_accounts/index.html.slim +25 -0
- data/app/views/weixin_pam/user_accounts/new.html.erb +5 -0
- data/app/views/weixin_pam/user_accounts/show.html.erb +29 -0
- data/config/initializers/simple_form.rb +165 -0
- data/config/initializers/simple_form_bootstrap.rb +149 -0
- data/config/initializers/weixin_rails_middleware.rb +27 -0
- data/config/locales/diymenu.zh-CN.yml +9 -0
- data/config/locales/public_account.zh-CN.yml +16 -0
- data/config/locales/simple_form.en.yml +31 -0
- data/config/locales/user_account.zh-CN.yml +13 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20151211153307_create_weixin_pam_public_accounts.rb +14 -0
- data/db/migrate/20151211153353_create_weixin_pam_user_accounts.rb +19 -0
- data/db/migrate/20151212152624_create_weixin_pam_diymenus.rb +18 -0
- data/db/migrate/20151215140830_add_weixin_secret_key_and_weixin_token_to_public_accounts.rb +13 -0
- data/db/migrate/20151218053505_add_host_to_weixin_pam_public_account.rb +6 -0
- data/lib/tasks/weixin_pam_tasks.rake +4 -0
- data/lib/templates/erb/scaffold/_form.html.erb +13 -0
- data/lib/weixin_pam.rb +3 -0
- data/lib/weixin_pam/api_error.rb +3 -0
- data/lib/weixin_pam/api_error/failed_result.rb +11 -0
- data/lib/weixin_pam/engine.rb +24 -0
- data/lib/weixin_pam/public_account_reply.rb +222 -0
- data/lib/weixin_pam/version.rb +3 -0
- data/test/controllers/weixin_pam/diymenus_controller_test.rb +52 -0
- data/test/controllers/weixin_pam/public_accounts_controller_test.rb +52 -0
- data/test/controllers/weixin_pam/user_accounts_controller_test.rb +52 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +26 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/schema.rb +60 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +594 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/fixtures/weixin_pam/diymenus.yml +19 -0
- data/test/fixtures/weixin_pam/public_accounts.yml +17 -0
- data/test/fixtures/weixin_pam/user_accounts.yml +15 -0
- data/test/integration/navigation_test.rb +8 -0
- data/test/models/weixin_pam/diymenu_test.rb +9 -0
- data/test/models/weixin_pam/public_account_test.rb +9 -0
- data/test/models/weixin_pam/user_account_test.rb +9 -0
- data/test/test_helper.rb +21 -0
- data/test/weixin_pam_test.rb +7 -0
- metadata +374 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: f677ffbc6f2a5589e10e63fc8960e640569a86bc
|
|
4
|
+
data.tar.gz: 1aee68abde79cf9acef34ac501219905cba3c5a2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4db095f8b09afd0284190486eb6984f3f71d5cc2bd547fd68eed3b454dfd5f096e7ef12780f27a3e4b2f10eeea16dc23422946569962093c06a84ba8b488b3ed
|
|
7
|
+
data.tar.gz: 2d9763357947eead939122e43f37032dae84655b54eb82ec039c4310b87b887535553b6c505f3df150850c9fdeb0ebe046bf964e563530bae755ce98315e5226
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2015 xiaohui
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# 微信公众号管理工具
|
|
2
|
+
## WeixinPam(Weixin Public Account Management)
|
|
3
|
+
|
|
4
|
+
本Rails Engine是为了方便公众号运维人员统一管理公众号的开发接口,不同公众号由同一个Rails管理和驱动,对不同的公众号,可实现如下功能:
|
|
5
|
+
|
|
6
|
+
1. 修改自定义菜单
|
|
7
|
+
2. 实现网页授权登录,获取用户信息并保存在数据库
|
|
8
|
+
3. 接收微信服务器推送信息,实现自动回复和其他高级功能
|
|
9
|
+
|
|
10
|
+
## 如何使用
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Gemfile:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
gem 'weixin_pem'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
config/routes:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
mount WeixinPam::Engine => '/'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
启动Rails server
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
bundle exec rails s
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
访问:http://localhost:3000/public_accounts 即可添加/删除公众号配置
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## 修改自定义菜单
|
|
35
|
+
|
|
36
|
+
点击已添加的公众号名字,点击“微信菜单”按钮,该页面可实现如下功能
|
|
37
|
+
|
|
38
|
+
1. 下载公众号菜单
|
|
39
|
+
2. 上传公众号菜单
|
|
40
|
+
3. 添加菜单到数据库
|
|
41
|
+
4. 移动菜单到“未启用的列表”
|
|
42
|
+
5. 对菜单排序
|
|
43
|
+
|
|
44
|
+
## 如何使用多公众号网页授权
|
|
45
|
+
|
|
46
|
+
本例子使用Devise作为用户登录模块,用到gem omniauth-wechat-oauth2
|
|
47
|
+
|
|
48
|
+
Gemfile中:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
gem 'devise'
|
|
52
|
+
gem "omniauth-wechat-oauth2"
|
|
53
|
+
```
|
|
54
|
+
config/initializers/devise.rb
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
require 'omniauth_setup'
|
|
58
|
+
# 此处 setup: OmniauthSetup是关键,他实现不同公众号的api身份切换
|
|
59
|
+
config.omniauth :wechat, nil, nil, setup: OmniauthSetup
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
lib/omniauth_setup.rb
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
class OmniauthSetup
|
|
66
|
+
# OmniAuth expects the class passed to setup to respond to the #call method.
|
|
67
|
+
# env - Rack environment
|
|
68
|
+
def self.call(env)
|
|
69
|
+
new(env).setup
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Assign variables and create a request object for use later.
|
|
73
|
+
# env - Rack environment
|
|
74
|
+
def initialize(env)
|
|
75
|
+
@env = env
|
|
76
|
+
@request = ActionDispatch::Request.new(env)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The main purpose of this method is to set the consumer key and secret.
|
|
80
|
+
def setup
|
|
81
|
+
@env['omniauth.strategy'].options.merge!(custom_credentials)
|
|
82
|
+
puts
|
|
83
|
+
puts @env['omniauth.strategy'].options.inspect
|
|
84
|
+
puts
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# Use the subdomain in the request to find the account with credentials
|
|
90
|
+
def custom_credentials
|
|
91
|
+
h = {}
|
|
92
|
+
scope = @request.params.delete(:scope).presence
|
|
93
|
+
h[:scope] = scope if scope
|
|
94
|
+
if account = WeixinPam::PublicAccount.find_by(host: @request.host)
|
|
95
|
+
h.update client_id: account.app_id, client_secret: account.app_secret
|
|
96
|
+
end
|
|
97
|
+
h
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
config/routes.rb
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
app/controllers/users/omniauth_callbacks_controller.rb
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|
113
|
+
def wechat
|
|
114
|
+
public_account = WeixinPam::PublicAccount.find_by!(host: request.host)
|
|
115
|
+
ua = WeixinPam::UserAccount.from_omniauth(public_account, request.env["omniauth.auth"])
|
|
116
|
+
if ua.user.persisted?
|
|
117
|
+
update_stored_location_url ua.user
|
|
118
|
+
sign_in_and_redirect ua.user, :event => :authentication
|
|
119
|
+
set_flash_message(:notice, :success, :kind => '微信') if is_navigational_format?
|
|
120
|
+
else
|
|
121
|
+
session["devise.wechat_data"] = request.env["omniauth.auth"]
|
|
122
|
+
redirect_to new_user_registration_url
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def update_stored_location_url(user)
|
|
129
|
+
if url = request.env['omniauth.params']['redirect'] || request.env['omniauth.origin']
|
|
130
|
+
store_location_for user, url
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
## 如何多公众号响应微信服务器事件推送
|
|
137
|
+
|
|
138
|
+
创建的公众号如果不填写"微信服务器事件推送的响应Class",WeixinPam会使用开发模式下的默认值PublicAccountReply(lib/public_account_reply.rb)
|
|
139
|
+
|
|
140
|
+
针对不同公众号,我们可以编写不同的Reply Class去继承PublicAccountReply,实现不同公众号有不同的回复内容。
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
可用属性 | 说明
|
|
144
|
+
---|---
|
|
145
|
+
weixin_public_account | 当前公众号WeixinPam::PublicAccount实例
|
|
146
|
+
weixin_user_account | 当前微信用户WeixinPam::UserAccount实例
|
|
147
|
+
weixin_message | 微信推送的消息
|
|
148
|
+
keyword | 用户发送的内容或事件的关键字
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## 用Devise保护资源
|
|
152
|
+
|
|
153
|
+
添加config/initializers/weixin_pam.rb,加入如下代码
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
WeixinPam::ApplicationController.class_eval do
|
|
157
|
+
before_action :authenticate_user!
|
|
158
|
+
before_action :ensure_admin_user
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def ensure_admin_user
|
|
163
|
+
fail "没有访问权限" unless current_user.admin?
|
|
164
|
+
end
|
|
165
|
+
end
|
data/Rakefile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
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 = 'WeixinPam'
|
|
12
|
+
rdoc.options << '--line-numbers'
|
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
APP_RAKEFILE = File.expand_path("../test/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
|
+
require 'rake/testtask'
|
|
28
|
+
|
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
|
30
|
+
t.libs << 'lib'
|
|
31
|
+
t.libs << 'test'
|
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
|
33
|
+
t.verbose = false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
task default: :test
|
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
/* ===================================================
|
|
2
|
+
* jquery-sortable.js v0.9.13
|
|
3
|
+
* http://johnny.github.com/jquery-sortable/
|
|
4
|
+
* ===================================================
|
|
5
|
+
* Copyright (c) 2012 Jonas von Andrian
|
|
6
|
+
* All rights reserved.
|
|
7
|
+
*
|
|
8
|
+
* Redistribution and use in source and binary forms, with or without
|
|
9
|
+
* modification, are permitted provided that the following conditions are met:
|
|
10
|
+
* * Redistributions of source code must retain the above copyright
|
|
11
|
+
* notice, this list of conditions and the following disclaimer.
|
|
12
|
+
* * Redistributions in binary form must reproduce the above copyright
|
|
13
|
+
* notice, this list of conditions and the following disclaimer in the
|
|
14
|
+
* documentation and/or other materials provided with the distribution.
|
|
15
|
+
* * The name of the author may not be used to endorse or promote products
|
|
16
|
+
* derived from this software without specific prior written permission.
|
|
17
|
+
*
|
|
18
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
19
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
20
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
|
22
|
+
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
23
|
+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
24
|
+
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
25
|
+
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
26
|
+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
27
|
+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
28
|
+
* ========================================================== */
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
!function ( $, window, pluginName, undefined){
|
|
32
|
+
var containerDefaults = {
|
|
33
|
+
// If true, items can be dragged from this container
|
|
34
|
+
drag: true,
|
|
35
|
+
// If true, items can be droped onto this container
|
|
36
|
+
drop: true,
|
|
37
|
+
// Exclude items from being draggable, if the
|
|
38
|
+
// selector matches the item
|
|
39
|
+
exclude: "",
|
|
40
|
+
// If true, search for nested containers within an item.If you nest containers,
|
|
41
|
+
// either the original selector with which you call the plugin must only match the top containers,
|
|
42
|
+
// or you need to specify a group (see the bootstrap nav example)
|
|
43
|
+
nested: true,
|
|
44
|
+
// If true, the items are assumed to be arranged vertically
|
|
45
|
+
vertical: true
|
|
46
|
+
}, // end container defaults
|
|
47
|
+
groupDefaults = {
|
|
48
|
+
// This is executed after the placeholder has been moved.
|
|
49
|
+
// $closestItemOrContainer contains the closest item, the placeholder
|
|
50
|
+
// has been put at or the closest empty Container, the placeholder has
|
|
51
|
+
// been appended to.
|
|
52
|
+
afterMove: function ($placeholder, container, $closestItemOrContainer) {
|
|
53
|
+
},
|
|
54
|
+
// The exact css path between the container and its items, e.g. "> tbody"
|
|
55
|
+
containerPath: "",
|
|
56
|
+
// The css selector of the containers
|
|
57
|
+
containerSelector: "ol, ul",
|
|
58
|
+
// Distance the mouse has to travel to start dragging
|
|
59
|
+
distance: 0,
|
|
60
|
+
// Time in milliseconds after mousedown until dragging should start.
|
|
61
|
+
// This option can be used to prevent unwanted drags when clicking on an element.
|
|
62
|
+
delay: 0,
|
|
63
|
+
// The css selector of the drag handle
|
|
64
|
+
handle: "",
|
|
65
|
+
// The exact css path between the item and its subcontainers.
|
|
66
|
+
// It should only match the immediate items of a container.
|
|
67
|
+
// No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div"
|
|
68
|
+
itemPath: "",
|
|
69
|
+
// The css selector of the items
|
|
70
|
+
itemSelector: "li",
|
|
71
|
+
// The class given to "body" while an item is being dragged
|
|
72
|
+
bodyClass: "dragging",
|
|
73
|
+
// The class giving to an item while being dragged
|
|
74
|
+
draggedClass: "dragged",
|
|
75
|
+
// Check if the dragged item may be inside the container.
|
|
76
|
+
// Use with care, since the search for a valid container entails a depth first search
|
|
77
|
+
// and may be quite expensive.
|
|
78
|
+
isValidTarget: function ($item, container) {
|
|
79
|
+
return true
|
|
80
|
+
},
|
|
81
|
+
// Executed before onDrop if placeholder is detached.
|
|
82
|
+
// This happens if pullPlaceholder is set to false and the drop occurs outside a container.
|
|
83
|
+
onCancel: function ($item, container, _super, event) {
|
|
84
|
+
},
|
|
85
|
+
// Executed at the beginning of a mouse move event.
|
|
86
|
+
// The Placeholder has not been moved yet.
|
|
87
|
+
onDrag: function ($item, position, _super, event) {
|
|
88
|
+
$item.css(position)
|
|
89
|
+
},
|
|
90
|
+
// Called after the drag has been started,
|
|
91
|
+
// that is the mouse button is being held down and
|
|
92
|
+
// the mouse is moving.
|
|
93
|
+
// The container is the closest initialized container.
|
|
94
|
+
// Therefore it might not be the container, that actually contains the item.
|
|
95
|
+
onDragStart: function ($item, container, _super, event) {
|
|
96
|
+
$item.css({
|
|
97
|
+
height: $item.outerHeight(),
|
|
98
|
+
width: $item.outerWidth()
|
|
99
|
+
})
|
|
100
|
+
$item.addClass(container.group.options.draggedClass)
|
|
101
|
+
$("body").addClass(container.group.options.bodyClass)
|
|
102
|
+
},
|
|
103
|
+
// Called when the mouse button is being released
|
|
104
|
+
onDrop: function ($item, container, _super, event) {
|
|
105
|
+
$item.removeClass(container.group.options.draggedClass).removeAttr("style")
|
|
106
|
+
$("body").removeClass(container.group.options.bodyClass)
|
|
107
|
+
},
|
|
108
|
+
// Called on mousedown. If falsy value is returned, the dragging will not start.
|
|
109
|
+
// Ignore if element clicked is input, select or textarea
|
|
110
|
+
onMousedown: function ($item, _super, event) {
|
|
111
|
+
if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) {
|
|
112
|
+
event.preventDefault()
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
// The class of the placeholder (must match placeholder option markup)
|
|
117
|
+
placeholderClass: "placeholder",
|
|
118
|
+
// Template for the placeholder. Can be any valid jQuery input
|
|
119
|
+
// e.g. a string, a DOM element.
|
|
120
|
+
// The placeholder must have the class "placeholder"
|
|
121
|
+
placeholder: '<li class="placeholder"></li>',
|
|
122
|
+
// If true, the position of the placeholder is calculated on every mousemove.
|
|
123
|
+
// If false, it is only calculated when the mouse is above a container.
|
|
124
|
+
pullPlaceholder: true,
|
|
125
|
+
// Specifies serialization of the container group.
|
|
126
|
+
// The pair $parent/$children is either container/items or item/subcontainers.
|
|
127
|
+
serialize: function ($parent, $children, parentIsContainer) {
|
|
128
|
+
var result = $.extend({}, $parent.data())
|
|
129
|
+
|
|
130
|
+
if(parentIsContainer)
|
|
131
|
+
return [$children]
|
|
132
|
+
else if ($children[0]){
|
|
133
|
+
result.children = $children
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
delete result.subContainers
|
|
137
|
+
delete result.sortable
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
},
|
|
141
|
+
// Set tolerance while dragging. Positive values decrease sensitivity,
|
|
142
|
+
// negative values increase it.
|
|
143
|
+
tolerance: 0
|
|
144
|
+
}, // end group defaults
|
|
145
|
+
containerGroups = {},
|
|
146
|
+
groupCounter = 0,
|
|
147
|
+
emptyBox = {
|
|
148
|
+
left: 0,
|
|
149
|
+
top: 0,
|
|
150
|
+
bottom: 0,
|
|
151
|
+
right:0
|
|
152
|
+
},
|
|
153
|
+
eventNames = {
|
|
154
|
+
start: "touchstart.sortable mousedown.sortable",
|
|
155
|
+
drop: "touchend.sortable touchcancel.sortable mouseup.sortable",
|
|
156
|
+
drag: "touchmove.sortable mousemove.sortable",
|
|
157
|
+
scroll: "scroll.sortable"
|
|
158
|
+
},
|
|
159
|
+
subContainerKey = "subContainers"
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
* a is Array [left, right, top, bottom]
|
|
163
|
+
* b is array [left, top]
|
|
164
|
+
*/
|
|
165
|
+
function d(a,b) {
|
|
166
|
+
var x = Math.max(0, a[0] - b[0], b[0] - a[1]),
|
|
167
|
+
y = Math.max(0, a[2] - b[1], b[1] - a[3])
|
|
168
|
+
return x+y;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function setDimensions(array, dimensions, tolerance, useOffset) {
|
|
172
|
+
var i = array.length,
|
|
173
|
+
offsetMethod = useOffset ? "offset" : "position"
|
|
174
|
+
tolerance = tolerance || 0
|
|
175
|
+
|
|
176
|
+
while(i--){
|
|
177
|
+
var el = array[i].el ? array[i].el : $(array[i]),
|
|
178
|
+
// use fitting method
|
|
179
|
+
pos = el[offsetMethod]()
|
|
180
|
+
pos.left += parseInt(el.css('margin-left'), 10)
|
|
181
|
+
pos.top += parseInt(el.css('margin-top'),10)
|
|
182
|
+
dimensions[i] = [
|
|
183
|
+
pos.left - tolerance,
|
|
184
|
+
pos.left + el.outerWidth() + tolerance,
|
|
185
|
+
pos.top - tolerance,
|
|
186
|
+
pos.top + el.outerHeight() + tolerance
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getRelativePosition(pointer, element) {
|
|
192
|
+
var offset = element.offset()
|
|
193
|
+
return {
|
|
194
|
+
left: pointer.left - offset.left,
|
|
195
|
+
top: pointer.top - offset.top
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sortByDistanceDesc(dimensions, pointer, lastPointer) {
|
|
200
|
+
pointer = [pointer.left, pointer.top]
|
|
201
|
+
lastPointer = lastPointer && [lastPointer.left, lastPointer.top]
|
|
202
|
+
|
|
203
|
+
var dim,
|
|
204
|
+
i = dimensions.length,
|
|
205
|
+
distances = []
|
|
206
|
+
|
|
207
|
+
while(i--){
|
|
208
|
+
dim = dimensions[i]
|
|
209
|
+
distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)]
|
|
210
|
+
}
|
|
211
|
+
distances = distances.sort(function (a,b) {
|
|
212
|
+
return b[1] - a[1] || b[2] - a[2] || b[0] - a[0]
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// last entry is the closest
|
|
216
|
+
return distances
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function ContainerGroup(options) {
|
|
220
|
+
this.options = $.extend({}, groupDefaults, options)
|
|
221
|
+
this.containers = []
|
|
222
|
+
|
|
223
|
+
if(!this.options.rootGroup){
|
|
224
|
+
this.scrollProxy = $.proxy(this.scroll, this)
|
|
225
|
+
this.dragProxy = $.proxy(this.drag, this)
|
|
226
|
+
this.dropProxy = $.proxy(this.drop, this)
|
|
227
|
+
this.placeholder = $(this.options.placeholder)
|
|
228
|
+
|
|
229
|
+
if(!options.isValidTarget)
|
|
230
|
+
this.options.isValidTarget = undefined
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
ContainerGroup.get = function (options) {
|
|
235
|
+
if(!containerGroups[options.group]) {
|
|
236
|
+
if(options.group === undefined)
|
|
237
|
+
options.group = groupCounter ++
|
|
238
|
+
|
|
239
|
+
containerGroups[options.group] = new ContainerGroup(options)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return containerGroups[options.group]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
ContainerGroup.prototype = {
|
|
246
|
+
dragInit: function (e, itemContainer) {
|
|
247
|
+
this.$document = $(itemContainer.el[0].ownerDocument)
|
|
248
|
+
|
|
249
|
+
// get item to drag
|
|
250
|
+
var closestItem = $(e.target).closest(this.options.itemSelector);
|
|
251
|
+
// using the length of this item, prevents the plugin from being started if there is no handle being clicked on.
|
|
252
|
+
// this may also be helpful in instantiating multidrag.
|
|
253
|
+
if (closestItem.length) {
|
|
254
|
+
this.item = closestItem;
|
|
255
|
+
this.itemContainer = itemContainer;
|
|
256
|
+
if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.setPointer(e);
|
|
260
|
+
this.toggleListeners('on');
|
|
261
|
+
this.setupDelayTimer();
|
|
262
|
+
this.dragInitDone = true;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
drag: function (e) {
|
|
266
|
+
if(!this.dragging){
|
|
267
|
+
if(!this.distanceMet(e) || !this.delayMet)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e)
|
|
271
|
+
this.item.before(this.placeholder)
|
|
272
|
+
this.dragging = true
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.setPointer(e)
|
|
276
|
+
// place item under the cursor
|
|
277
|
+
this.options.onDrag(this.item,
|
|
278
|
+
getRelativePosition(this.pointer, this.item.offsetParent()),
|
|
279
|
+
groupDefaults.onDrag,
|
|
280
|
+
e)
|
|
281
|
+
|
|
282
|
+
var p = this.getPointer(e),
|
|
283
|
+
box = this.sameResultBox,
|
|
284
|
+
t = this.options.tolerance
|
|
285
|
+
|
|
286
|
+
if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left)
|
|
287
|
+
if(!this.searchValidTarget()){
|
|
288
|
+
this.placeholder.detach()
|
|
289
|
+
this.lastAppendedItem = undefined
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
drop: function (e) {
|
|
293
|
+
this.toggleListeners('off')
|
|
294
|
+
|
|
295
|
+
this.dragInitDone = false
|
|
296
|
+
|
|
297
|
+
if(this.dragging){
|
|
298
|
+
// processing Drop, check if placeholder is detached
|
|
299
|
+
if(this.placeholder.closest("html")[0]){
|
|
300
|
+
this.placeholder.before(this.item).detach()
|
|
301
|
+
} else {
|
|
302
|
+
this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e)
|
|
303
|
+
}
|
|
304
|
+
this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e)
|
|
305
|
+
|
|
306
|
+
// cleanup
|
|
307
|
+
this.clearDimensions()
|
|
308
|
+
this.clearOffsetParent()
|
|
309
|
+
this.lastAppendedItem = this.sameResultBox = undefined
|
|
310
|
+
this.dragging = false
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
searchValidTarget: function (pointer, lastPointer) {
|
|
314
|
+
if(!pointer){
|
|
315
|
+
pointer = this.relativePointer || this.pointer
|
|
316
|
+
lastPointer = this.lastRelativePointer || this.lastPointer
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
var distances = sortByDistanceDesc(this.getContainerDimensions(),
|
|
320
|
+
pointer,
|
|
321
|
+
lastPointer),
|
|
322
|
+
i = distances.length
|
|
323
|
+
|
|
324
|
+
while(i--){
|
|
325
|
+
var index = distances[i][0],
|
|
326
|
+
distance = distances[i][1]
|
|
327
|
+
|
|
328
|
+
if(!distance || this.options.pullPlaceholder){
|
|
329
|
+
var container = this.containers[index]
|
|
330
|
+
if(!container.disabled){
|
|
331
|
+
if(!this.$getOffsetParent()){
|
|
332
|
+
var offsetParent = container.getItemOffsetParent()
|
|
333
|
+
pointer = getRelativePosition(pointer, offsetParent)
|
|
334
|
+
lastPointer = getRelativePosition(lastPointer, offsetParent)
|
|
335
|
+
}
|
|
336
|
+
if(container.searchValidTarget(pointer, lastPointer))
|
|
337
|
+
return true
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if(this.sameResultBox)
|
|
342
|
+
this.sameResultBox = undefined
|
|
343
|
+
},
|
|
344
|
+
movePlaceholder: function (container, item, method, sameResultBox) {
|
|
345
|
+
var lastAppendedItem = this.lastAppendedItem
|
|
346
|
+
if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0])
|
|
347
|
+
return;
|
|
348
|
+
|
|
349
|
+
item[method](this.placeholder)
|
|
350
|
+
this.lastAppendedItem = item
|
|
351
|
+
this.sameResultBox = sameResultBox
|
|
352
|
+
this.options.afterMove(this.placeholder, container, item)
|
|
353
|
+
},
|
|
354
|
+
getContainerDimensions: function () {
|
|
355
|
+
if(!this.containerDimensions)
|
|
356
|
+
setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent())
|
|
357
|
+
return this.containerDimensions
|
|
358
|
+
},
|
|
359
|
+
getContainer: function (element) {
|
|
360
|
+
return element.closest(this.options.containerSelector).data(pluginName)
|
|
361
|
+
},
|
|
362
|
+
$getOffsetParent: function () {
|
|
363
|
+
if(this.offsetParent === undefined){
|
|
364
|
+
var i = this.containers.length - 1,
|
|
365
|
+
offsetParent = this.containers[i].getItemOffsetParent()
|
|
366
|
+
|
|
367
|
+
if(!this.options.rootGroup){
|
|
368
|
+
while(i--){
|
|
369
|
+
if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){
|
|
370
|
+
// If every container has the same offset parent,
|
|
371
|
+
// use position() which is relative to this parent,
|
|
372
|
+
// otherwise use offset()
|
|
373
|
+
// compare #setDimensions
|
|
374
|
+
offsetParent = false
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.offsetParent = offsetParent
|
|
381
|
+
}
|
|
382
|
+
return this.offsetParent
|
|
383
|
+
},
|
|
384
|
+
setPointer: function (e) {
|
|
385
|
+
var pointer = this.getPointer(e)
|
|
386
|
+
|
|
387
|
+
if(this.$getOffsetParent()){
|
|
388
|
+
var relativePointer = getRelativePosition(pointer, this.$getOffsetParent())
|
|
389
|
+
this.lastRelativePointer = this.relativePointer
|
|
390
|
+
this.relativePointer = relativePointer
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.lastPointer = this.pointer
|
|
394
|
+
this.pointer = pointer
|
|
395
|
+
},
|
|
396
|
+
distanceMet: function (e) {
|
|
397
|
+
var currentPointer = this.getPointer(e)
|
|
398
|
+
return (Math.max(
|
|
399
|
+
Math.abs(this.pointer.left - currentPointer.left),
|
|
400
|
+
Math.abs(this.pointer.top - currentPointer.top)
|
|
401
|
+
) >= this.options.distance)
|
|
402
|
+
},
|
|
403
|
+
getPointer: function(e) {
|
|
404
|
+
var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0]
|
|
405
|
+
return {
|
|
406
|
+
left: e.pageX || o.pageX,
|
|
407
|
+
top: e.pageY || o.pageY
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
setupDelayTimer: function () {
|
|
411
|
+
var that = this
|
|
412
|
+
this.delayMet = !this.options.delay
|
|
413
|
+
|
|
414
|
+
// init delay timer if needed
|
|
415
|
+
if (!this.delayMet) {
|
|
416
|
+
clearTimeout(this._mouseDelayTimer);
|
|
417
|
+
this._mouseDelayTimer = setTimeout(function() {
|
|
418
|
+
that.delayMet = true
|
|
419
|
+
}, this.options.delay)
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
scroll: function (e) {
|
|
423
|
+
this.clearDimensions()
|
|
424
|
+
this.clearOffsetParent() // TODO is this needed?
|
|
425
|
+
},
|
|
426
|
+
toggleListeners: function (method) {
|
|
427
|
+
var that = this,
|
|
428
|
+
events = ['drag','drop','scroll']
|
|
429
|
+
|
|
430
|
+
$.each(events,function (i,event) {
|
|
431
|
+
that.$document[method](eventNames[event], that[event + 'Proxy'])
|
|
432
|
+
})
|
|
433
|
+
},
|
|
434
|
+
clearOffsetParent: function () {
|
|
435
|
+
this.offsetParent = undefined
|
|
436
|
+
},
|
|
437
|
+
// Recursively clear container and item dimensions
|
|
438
|
+
clearDimensions: function () {
|
|
439
|
+
this.traverse(function(object){
|
|
440
|
+
object._clearDimensions()
|
|
441
|
+
})
|
|
442
|
+
},
|
|
443
|
+
traverse: function(callback) {
|
|
444
|
+
callback(this)
|
|
445
|
+
var i = this.containers.length
|
|
446
|
+
while(i--){
|
|
447
|
+
this.containers[i].traverse(callback)
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
_clearDimensions: function(){
|
|
451
|
+
this.containerDimensions = undefined
|
|
452
|
+
},
|
|
453
|
+
_destroy: function () {
|
|
454
|
+
containerGroups[this.options.group] = undefined
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function Container(element, options) {
|
|
459
|
+
this.el = element
|
|
460
|
+
this.options = $.extend( {}, containerDefaults, options)
|
|
461
|
+
|
|
462
|
+
this.group = ContainerGroup.get(this.options)
|
|
463
|
+
this.rootGroup = this.options.rootGroup || this.group
|
|
464
|
+
this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector
|
|
465
|
+
|
|
466
|
+
var itemPath = this.rootGroup.options.itemPath
|
|
467
|
+
this.target = itemPath ? this.el.find(itemPath) : this.el
|
|
468
|
+
|
|
469
|
+
this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this))
|
|
470
|
+
|
|
471
|
+
if(this.options.drop)
|
|
472
|
+
this.group.containers.push(this)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
Container.prototype = {
|
|
476
|
+
dragInit: function (e) {
|
|
477
|
+
var rootGroup = this.rootGroup
|
|
478
|
+
|
|
479
|
+
if( !this.disabled &&
|
|
480
|
+
!rootGroup.dragInitDone &&
|
|
481
|
+
this.options.drag &&
|
|
482
|
+
this.isValidDrag(e)) {
|
|
483
|
+
rootGroup.dragInit(e, this)
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
isValidDrag: function(e) {
|
|
487
|
+
return e.which == 1 ||
|
|
488
|
+
e.type == "touchstart" && e.originalEvent.touches.length == 1
|
|
489
|
+
},
|
|
490
|
+
searchValidTarget: function (pointer, lastPointer) {
|
|
491
|
+
var distances = sortByDistanceDesc(this.getItemDimensions(),
|
|
492
|
+
pointer,
|
|
493
|
+
lastPointer),
|
|
494
|
+
i = distances.length,
|
|
495
|
+
rootGroup = this.rootGroup,
|
|
496
|
+
validTarget = !rootGroup.options.isValidTarget ||
|
|
497
|
+
rootGroup.options.isValidTarget(rootGroup.item, this)
|
|
498
|
+
|
|
499
|
+
if(!i && validTarget){
|
|
500
|
+
rootGroup.movePlaceholder(this, this.target, "append")
|
|
501
|
+
return true
|
|
502
|
+
} else
|
|
503
|
+
while(i--){
|
|
504
|
+
var index = distances[i][0],
|
|
505
|
+
distance = distances[i][1]
|
|
506
|
+
if(!distance && this.hasChildGroup(index)){
|
|
507
|
+
var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer)
|
|
508
|
+
if(found)
|
|
509
|
+
return true
|
|
510
|
+
}
|
|
511
|
+
else if(validTarget){
|
|
512
|
+
this.movePlaceholder(index, pointer)
|
|
513
|
+
return true
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
movePlaceholder: function (index, pointer) {
|
|
518
|
+
var item = $(this.items[index]),
|
|
519
|
+
dim = this.itemDimensions[index],
|
|
520
|
+
method = "after",
|
|
521
|
+
width = item.outerWidth(),
|
|
522
|
+
height = item.outerHeight(),
|
|
523
|
+
offset = item.offset(),
|
|
524
|
+
sameResultBox = {
|
|
525
|
+
left: offset.left,
|
|
526
|
+
right: offset.left + width,
|
|
527
|
+
top: offset.top,
|
|
528
|
+
bottom: offset.top + height
|
|
529
|
+
}
|
|
530
|
+
if(this.options.vertical){
|
|
531
|
+
var yCenter = (dim[2] + dim[3]) / 2,
|
|
532
|
+
inUpperHalf = pointer.top <= yCenter
|
|
533
|
+
if(inUpperHalf){
|
|
534
|
+
method = "before"
|
|
535
|
+
sameResultBox.bottom -= height / 2
|
|
536
|
+
} else
|
|
537
|
+
sameResultBox.top += height / 2
|
|
538
|
+
} else {
|
|
539
|
+
var xCenter = (dim[0] + dim[1]) / 2,
|
|
540
|
+
inLeftHalf = pointer.left <= xCenter
|
|
541
|
+
if(inLeftHalf){
|
|
542
|
+
method = "before"
|
|
543
|
+
sameResultBox.right -= width / 2
|
|
544
|
+
} else
|
|
545
|
+
sameResultBox.left += width / 2
|
|
546
|
+
}
|
|
547
|
+
if(this.hasChildGroup(index))
|
|
548
|
+
sameResultBox = emptyBox
|
|
549
|
+
this.rootGroup.movePlaceholder(this, item, method, sameResultBox)
|
|
550
|
+
},
|
|
551
|
+
getItemDimensions: function () {
|
|
552
|
+
if(!this.itemDimensions){
|
|
553
|
+
this.items = this.$getChildren(this.el, "item").filter(
|
|
554
|
+
":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")"
|
|
555
|
+
).get()
|
|
556
|
+
setDimensions(this.items, this.itemDimensions = [], this.options.tolerance)
|
|
557
|
+
}
|
|
558
|
+
return this.itemDimensions
|
|
559
|
+
},
|
|
560
|
+
getItemOffsetParent: function () {
|
|
561
|
+
var offsetParent,
|
|
562
|
+
el = this.el
|
|
563
|
+
// Since el might be empty we have to check el itself and
|
|
564
|
+
// can not do something like el.children().first().offsetParent()
|
|
565
|
+
if(el.css("position") === "relative" || el.css("position") === "absolute" || el.css("position") === "fixed")
|
|
566
|
+
offsetParent = el
|
|
567
|
+
else
|
|
568
|
+
offsetParent = el.offsetParent()
|
|
569
|
+
return offsetParent
|
|
570
|
+
},
|
|
571
|
+
hasChildGroup: function (index) {
|
|
572
|
+
return this.options.nested && this.getContainerGroup(index)
|
|
573
|
+
},
|
|
574
|
+
getContainerGroup: function (index) {
|
|
575
|
+
var childGroup = $.data(this.items[index], subContainerKey)
|
|
576
|
+
if( childGroup === undefined){
|
|
577
|
+
var childContainers = this.$getChildren(this.items[index], "container")
|
|
578
|
+
childGroup = false
|
|
579
|
+
|
|
580
|
+
if(childContainers[0]){
|
|
581
|
+
var options = $.extend({}, this.options, {
|
|
582
|
+
rootGroup: this.rootGroup,
|
|
583
|
+
group: groupCounter ++
|
|
584
|
+
})
|
|
585
|
+
childGroup = childContainers[pluginName](options).data(pluginName).group
|
|
586
|
+
}
|
|
587
|
+
$.data(this.items[index], subContainerKey, childGroup)
|
|
588
|
+
}
|
|
589
|
+
return childGroup
|
|
590
|
+
},
|
|
591
|
+
$getChildren: function (parent, type) {
|
|
592
|
+
var options = this.rootGroup.options,
|
|
593
|
+
path = options[type + "Path"],
|
|
594
|
+
selector = options[type + "Selector"]
|
|
595
|
+
|
|
596
|
+
parent = $(parent)
|
|
597
|
+
if(path)
|
|
598
|
+
parent = parent.find(path)
|
|
599
|
+
|
|
600
|
+
return parent.children(selector)
|
|
601
|
+
},
|
|
602
|
+
_serialize: function (parent, isContainer) {
|
|
603
|
+
var that = this,
|
|
604
|
+
childType = isContainer ? "item" : "container",
|
|
605
|
+
|
|
606
|
+
children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () {
|
|
607
|
+
return that._serialize($(this), !isContainer)
|
|
608
|
+
}).get()
|
|
609
|
+
|
|
610
|
+
return this.rootGroup.options.serialize(parent, children, isContainer)
|
|
611
|
+
},
|
|
612
|
+
traverse: function(callback) {
|
|
613
|
+
$.each(this.items || [], function(item){
|
|
614
|
+
var group = $.data(this, subContainerKey)
|
|
615
|
+
if(group)
|
|
616
|
+
group.traverse(callback)
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
callback(this)
|
|
620
|
+
},
|
|
621
|
+
_clearDimensions: function () {
|
|
622
|
+
this.itemDimensions = undefined
|
|
623
|
+
},
|
|
624
|
+
_destroy: function() {
|
|
625
|
+
console.log('destroy in container')
|
|
626
|
+
var that = this;
|
|
627
|
+
|
|
628
|
+
this.target.off(eventNames.start, this.handle);
|
|
629
|
+
this.el.removeData(pluginName)
|
|
630
|
+
|
|
631
|
+
if(this.options.drop)
|
|
632
|
+
this.group.containers = $.grep(this.group.containers, function(val){
|
|
633
|
+
return val != that
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
$.each(this.items || [], function(){
|
|
637
|
+
$.removeData(this, subContainerKey)
|
|
638
|
+
})
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
var API = {
|
|
643
|
+
enable: function() {
|
|
644
|
+
this.traverse(function(object){
|
|
645
|
+
object.disabled = false
|
|
646
|
+
})
|
|
647
|
+
},
|
|
648
|
+
disable: function (){
|
|
649
|
+
this.traverse(function(object){
|
|
650
|
+
object.disabled = true
|
|
651
|
+
})
|
|
652
|
+
},
|
|
653
|
+
serialize: function () {
|
|
654
|
+
return this._serialize(this.el, true)
|
|
655
|
+
},
|
|
656
|
+
refresh: function() {
|
|
657
|
+
this.traverse(function(object){
|
|
658
|
+
object._clearDimensions()
|
|
659
|
+
})
|
|
660
|
+
},
|
|
661
|
+
destroy: function () {
|
|
662
|
+
this.traverse(function(object){
|
|
663
|
+
object._destroy();
|
|
664
|
+
})
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
$.extend(Container.prototype, API)
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* jQuery API
|
|
672
|
+
*
|
|
673
|
+
* Parameters are
|
|
674
|
+
* either options on init
|
|
675
|
+
* or a method name followed by arguments to pass to the method
|
|
676
|
+
*/
|
|
677
|
+
$.fn[pluginName] = function(methodOrOptions) {
|
|
678
|
+
var args = Array.prototype.slice.call(arguments, 1)
|
|
679
|
+
|
|
680
|
+
// clear containerGroups for turbolinks
|
|
681
|
+
if(typeof methodOrOptions === 'object' && methodOrOptions.clear){
|
|
682
|
+
containerGroups = [];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return this.map(function(){
|
|
686
|
+
var $t = $(this),
|
|
687
|
+
object = $t.data(pluginName)
|
|
688
|
+
|
|
689
|
+
if(object && API[methodOrOptions])
|
|
690
|
+
return API[methodOrOptions].apply(object, args) || this
|
|
691
|
+
else if(!object && (methodOrOptions === undefined ||
|
|
692
|
+
typeof methodOrOptions === "object"))
|
|
693
|
+
$t.data(pluginName, new Container($t, methodOrOptions))
|
|
694
|
+
|
|
695
|
+
return this
|
|
696
|
+
});
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
}(jQuery, window, 'sortable');
|