blazer 0.0.8 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of blazer might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +261 -45
- data/app/assets/javascripts/blazer/Sortable.js +1144 -0
- data/app/assets/javascripts/blazer/application.js +2 -1
- data/app/assets/javascripts/blazer/chartkick.js +935 -0
- data/app/assets/javascripts/blazer/selectize.js +391 -201
- data/app/assets/stylesheets/blazer/application.css +17 -2
- data/app/assets/stylesheets/blazer/selectize.default.css +3 -2
- data/app/controllers/blazer/base_controller.rb +48 -0
- data/app/controllers/blazer/checks_controller.rb +51 -0
- data/app/controllers/blazer/dashboards_controller.rb +94 -0
- data/app/controllers/blazer/queries_controller.rb +29 -101
- data/app/helpers/blazer/{queries_helper.rb → base_helper.rb} +1 -1
- data/app/mailers/blazer/check_mailer.rb +21 -0
- data/app/models/blazer/check.rb +28 -0
- data/app/models/blazer/connection.rb +0 -1
- data/app/models/blazer/dashboard.rb +12 -0
- data/app/models/blazer/dashboard_query.rb +9 -0
- data/app/models/blazer/query.rb +5 -0
- data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
- data/app/views/blazer/check_mailer/state_change.html.erb +6 -0
- data/app/views/blazer/checks/_form.html.erb +28 -0
- data/app/views/blazer/checks/edit.html.erb +1 -0
- data/app/views/blazer/checks/index.html.erb +41 -0
- data/app/views/blazer/checks/new.html.erb +1 -0
- data/app/views/blazer/checks/run.html.erb +9 -0
- data/app/views/blazer/dashboards/_form.html.erb +86 -0
- data/app/views/blazer/dashboards/edit.html.erb +1 -0
- data/app/views/blazer/dashboards/index.html.erb +21 -0
- data/app/views/blazer/dashboards/new.html.erb +1 -0
- data/app/views/blazer/dashboards/show.html.erb +148 -0
- data/app/views/blazer/queries/_form.html.erb +16 -5
- data/app/views/blazer/queries/_tables.html +5 -0
- data/app/views/blazer/queries/index.html.erb +6 -0
- data/app/views/blazer/queries/run.html.erb +59 -44
- data/app/views/blazer/queries/show.html.erb +20 -16
- data/config/routes.rb +5 -0
- data/lib/blazer.rb +46 -2
- data/lib/blazer/data_source.rb +70 -0
- data/lib/blazer/engine.rb +6 -2
- data/lib/blazer/tasks.rb +12 -0
- data/lib/blazer/version.rb +1 -1
- data/lib/generators/blazer/templates/config.yml +26 -6
- data/lib/generators/blazer/templates/install.rb +21 -0
- metadata +27 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f6da26ecc0d5ab8855037e401139fe007d60688
|
4
|
+
data.tar.gz: 9c1346b367f96b89240ae6c396dae6d9fd0f4f60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d1d3782d208f2c8b00bc5bedb1d4ecbaee6e7d23cb4d90f8a76348f061d4601c9d5d8c0061dc6cc79944d78897177eb95a026b1d5939aa32575d2b2a5bf0ddc
|
7
|
+
data.tar.gz: d51440f5c1890e0eed340ec6d2d5dc974286c0f1af1781a8294e2726525a9a3b54a0e5d7f03f160ce976d01795007e7adf91a59422cab4844ddeecbaf24a6acd
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,26 +1,30 @@
|
|
1
1
|
# Blazer
|
2
2
|
|
3
|
-
|
3
|
+
Explore your data with SQL. Easily create charts and dashboards, and share them with your team.
|
4
4
|
|
5
|
-
|
5
|
+
[Try it out](http://blazer.hero2app.com)
|
6
6
|
|
7
|
-
[
|
8
|
-
|
9
|
-
[![Screenshot](https://blazerme.herokuapp.com/assets/screenshot-18d79092e635b4b220f57ff7a1ecea41.png)](https://blazerme.herokuapp.com)
|
10
|
-
|
11
|
-
Works with PostgreSQL and MySQL
|
7
|
+
[![Screenshot](http://blazer.hero2app.com/assets/screenshot-473b2307ebaf7377645079cbf2badf73.png)](http://blazer.hero2app.com)
|
12
8
|
|
13
9
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
14
10
|
|
11
|
+
**Blazer 1.0 was recently released!** See the [instructions for upgrading](#10).
|
12
|
+
|
15
13
|
## Features
|
16
14
|
|
17
|
-
- **
|
15
|
+
- **Multiple data sources** - works with PostgreSQL, MySQL, and Redshift
|
18
16
|
- **Variables** - run the same queries with different values
|
19
|
-
- **
|
20
|
-
- **Smart Columns** - get the data you want without all the joins
|
21
|
-
- **Smart Variables** - no need to remember ids
|
22
|
-
- **Charts** - visualize the data
|
17
|
+
- **Checks & alerts** - get emailed when bad data appears
|
23
18
|
- **Audits** - all queries are tracked
|
19
|
+
- **Security** - works with your authentication system
|
20
|
+
|
21
|
+
## Docs
|
22
|
+
|
23
|
+
- [Installation](#installation)
|
24
|
+
- [Queries](#queries)
|
25
|
+
- [Charts](#charts)
|
26
|
+
- [Dashboards](#dashboards)
|
27
|
+
- [Checks](#checks)
|
24
28
|
|
25
29
|
## Installation
|
26
30
|
|
@@ -46,10 +50,30 @@ mount Blazer::Engine, at: "blazer"
|
|
46
50
|
For production, specify your database:
|
47
51
|
|
48
52
|
```ruby
|
49
|
-
ENV["BLAZER_DATABASE_URL"] = "postgres://user:password@hostname:5432/
|
53
|
+
ENV["BLAZER_DATABASE_URL"] = "postgres://user:password@hostname:5432/database"
|
54
|
+
```
|
55
|
+
|
56
|
+
Blazer tries to protect against queries which modify data (by running each query in a transaction and rolling it back), but a safer approach is to use a read only user. [See how to create one](#permissions).
|
57
|
+
|
58
|
+
#### Checks (optional)
|
59
|
+
|
60
|
+
Be sure to set a host in `config/environments/production.rb` for emails to work.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
config.action_mailer.default_url_options = {host: "blazerme.herokuapp.com"}
|
64
|
+
```
|
65
|
+
|
66
|
+
Schedule checks to run every hour (with cron, [Heroku Scheduler](https://addons.heroku.com/scheduler), etc).
|
67
|
+
|
68
|
+
```sh
|
69
|
+
rake blazer:run_checks
|
50
70
|
```
|
51
71
|
|
52
|
-
|
72
|
+
You can also set up failing checks to be sent once a day (or whatever you prefer).
|
73
|
+
|
74
|
+
```sh
|
75
|
+
rake blazer:send_failing_checks
|
76
|
+
```
|
53
77
|
|
54
78
|
## Permissions
|
55
79
|
|
@@ -67,8 +91,6 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO blazer;
|
|
67
91
|
COMMIT;
|
68
92
|
```
|
69
93
|
|
70
|
-
It is **highly, highly recommended** to protect sensitive information with views. Documentation coming soon.
|
71
|
-
|
72
94
|
### MySQL
|
73
95
|
|
74
96
|
Create a user with read only permissions:
|
@@ -78,7 +100,9 @@ GRANT SELECT, SHOW VIEW ON database_name.* TO blazer@’127.0.0.1′ IDENTIFIED
|
|
78
100
|
FLUSH PRIVILEGES;
|
79
101
|
```
|
80
102
|
|
81
|
-
|
103
|
+
### Sensitive Data
|
104
|
+
|
105
|
+
To protect sensitive info like password hashes and access tokens, use views. Documentation coming soon.
|
82
106
|
|
83
107
|
## Authentication
|
84
108
|
|
@@ -96,66 +120,256 @@ ENV["BLAZER_PASSWORD"] = "secret"
|
|
96
120
|
### Devise
|
97
121
|
|
98
122
|
```ruby
|
99
|
-
authenticate :user, lambda{|user| user.admin? } do
|
123
|
+
authenticate :user, lambda { |user| user.admin? } do
|
100
124
|
mount Blazer::Engine, at: "blazer"
|
101
125
|
end
|
102
126
|
```
|
103
127
|
|
104
|
-
##
|
128
|
+
## Queries
|
105
129
|
|
106
|
-
|
130
|
+
### Variables
|
107
131
|
|
108
|
-
|
109
|
-
|
132
|
+
Create queries with variables.
|
133
|
+
|
134
|
+
```sql
|
135
|
+
SELECT * FROM users WHERE gender = {gender}
|
110
136
|
```
|
111
137
|
|
112
|
-
|
138
|
+
Use `{start_time}` and `{end_time}` for time ranges. [Example](http://blazer.hero2app.com/queries/8-ratings-by-time-range?start_time=1997-10-03T05%3A00%3A00%2B00%3A00&end_time=1997-10-04T04%3A59%3A59%2B00%3A00)
|
113
139
|
|
114
|
-
```
|
115
|
-
|
140
|
+
```sql
|
141
|
+
SELECT * FROM ratings WHERE rated_at >= {start_time} AND rated_at <= {end_time}
|
116
142
|
```
|
117
143
|
|
118
|
-
|
144
|
+
### Smart Variables
|
119
145
|
|
120
|
-
|
121
|
-
|
146
|
+
[Example](http://blazer.hero2app.com/queries/9-movies-by-genre)
|
147
|
+
|
148
|
+
Suppose you have the query:
|
149
|
+
|
150
|
+
```sql
|
151
|
+
SELECT * FROM users WHERE city_id = {city_id}
|
122
152
|
```
|
123
153
|
|
124
|
-
|
154
|
+
Instead of remembering each city’s id, users can select cities by name.
|
125
155
|
|
126
|
-
|
127
|
-
|
156
|
+
Add a smart variable with:
|
157
|
+
|
158
|
+
```yml
|
159
|
+
smart_variables:
|
160
|
+
city_id: "SELECT id, name FROM cities ORDER BY name ASC"
|
128
161
|
```
|
129
162
|
|
130
|
-
|
163
|
+
The first column is the value of the variable, and the second column is the label.
|
131
164
|
|
132
|
-
|
133
|
-
|
165
|
+
### Linked Columns
|
166
|
+
|
167
|
+
[Example](http://blazer.hero2app.com/queries/4-highest-rated-movies) - title column
|
168
|
+
|
169
|
+
Link results to other pages in your apps or around the web. Specify a column name and where it should link to. You can use the value of the result with `{value}`.
|
170
|
+
|
171
|
+
```yml
|
172
|
+
linked_columns:
|
173
|
+
user_id: "/admin/users/{value}"
|
174
|
+
ip_address: "http://www.infosniper.net/index.php?ip_address={value}"
|
175
|
+
```
|
176
|
+
|
177
|
+
### Smart Columns
|
178
|
+
|
179
|
+
[Example](http://blazer.hero2app.com/queries/11-users) - occupation_id column
|
180
|
+
|
181
|
+
Suppose you have the query:
|
182
|
+
|
183
|
+
```sql
|
184
|
+
SELECT name, city_id FROM users
|
185
|
+
```
|
186
|
+
|
187
|
+
See which city the user belongs to without a join.
|
188
|
+
|
189
|
+
```yml
|
190
|
+
smart_columns:
|
191
|
+
city_id: "SELECT id, name FROM cities WHERE id IN {value}"
|
134
192
|
```
|
135
193
|
|
136
194
|
## Charts
|
137
195
|
|
138
|
-
Blazer will automatically generate charts based on the types of the columns returned in your query
|
196
|
+
Blazer will automatically generate charts based on the types of the columns returned in your query.
|
139
197
|
|
140
198
|
### Line Chart
|
141
199
|
|
142
|
-
|
200
|
+
There are two ways to generate line charts.
|
201
|
+
|
202
|
+
2+ columns - timestamp, numeric(s) - [Example](http://blazer.hero2app.com/queries/1-new-ratings-per-week)
|
203
|
+
|
204
|
+
```sql
|
205
|
+
SELECT date_trunc('week', created_at), COUNT(*) FROM users GROUP BY 1
|
206
|
+
```
|
207
|
+
|
208
|
+
3 columns - timestamp, string, numeric - [Example](http://blazer.hero2app.com/queries/7-new-ratings-by-gender-per-month)
|
209
|
+
|
210
|
+
|
211
|
+
```sql
|
212
|
+
SELECT date_trunc('week', created_at), gender, COUNT(*) FROM users GROUP BY 1, 2
|
213
|
+
```
|
143
214
|
|
144
215
|
### Pie Chart
|
145
216
|
|
146
|
-
|
217
|
+
2 columns - string, numeric - [Example](http://blazer.hero2app.com/queries/2-top-genres)
|
218
|
+
|
219
|
+
```sql
|
220
|
+
SELECT gender, COUNT(*) FROM users GROUP BY 1
|
221
|
+
```
|
222
|
+
|
223
|
+
## Dashboards
|
224
|
+
|
225
|
+
Create a dashboard with multiple queries. [Example](http://blazer.hero2app.com/dashboards/1-movielens)
|
226
|
+
|
227
|
+
If the query has a chart, the chart is shown. Otherwise, you’ll see a table.
|
228
|
+
|
229
|
+
If any queries have variables, they will show up on the dashboard.
|
230
|
+
|
231
|
+
## Checks
|
232
|
+
|
233
|
+
Checks give you a centralized place to see the health of your data. [Example](http://blazer.hero2app.com/checks)
|
234
|
+
|
235
|
+
Create a query to identify bad rows.
|
236
|
+
|
237
|
+
```sql
|
238
|
+
SELECT * FROM ratings WHERE user_id IS NULL /* all ratings should have a user */
|
239
|
+
```
|
240
|
+
|
241
|
+
Then create check with optional emails if you want to be notified. Emails are sent when a check starts failing, and when it starts passing again.
|
242
|
+
|
243
|
+
## Data Sources
|
244
|
+
|
245
|
+
Blazer supports multiple data sources :tada:
|
246
|
+
|
247
|
+
Add additional data sources in `config/blazer.yml`:
|
248
|
+
|
249
|
+
```yml
|
250
|
+
data_sources:
|
251
|
+
main:
|
252
|
+
url: <%= ENV["BLAZER_DATABASE_URL"] %>
|
253
|
+
# timeout, smart_variables, linked_columns, smart_columns
|
254
|
+
catalog:
|
255
|
+
url: <%= ENV["CATALOG_DATABASE_URL"] %>
|
256
|
+
# ...
|
257
|
+
redshift:
|
258
|
+
url: <%= ENV["REDSHIFT_DATABASE_URL"] %>
|
259
|
+
# ...
|
260
|
+
```
|
261
|
+
|
262
|
+
### Redshift
|
263
|
+
|
264
|
+
Add [activerecord4-redshift-adapter](https://github.com/aamine/activerecord4-redshift-adapter) to your Gemfile and set:
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
ENV["BLAZER_DATABASE_URL"] = "redshift://user:password@hostname:5439/database"
|
268
|
+
```
|
269
|
+
|
270
|
+
## Learn SQL
|
271
|
+
|
272
|
+
Have team members who want to learn SQL? Here are a few great, free resources.
|
273
|
+
|
274
|
+
- [Khan Academy](https://www.khanacademy.org/computing/computer-programming/sql)
|
275
|
+
- [Codecademy](https://www.codecademy.com/courses/learn-sql)
|
276
|
+
|
277
|
+
## Useful Tools
|
278
|
+
|
279
|
+
For an easy way to group by day, week, month, and more with correct time zones, check out [Groupdate](https://github.com/ankane/groupdate.sql).
|
280
|
+
|
281
|
+
## Upgrading
|
282
|
+
|
283
|
+
### 1.0
|
284
|
+
|
285
|
+
Blazer 1.0 brings a number of new features:
|
286
|
+
|
287
|
+
- multiple data sources, including Redshift
|
288
|
+
- dashboards
|
289
|
+
- checks
|
290
|
+
|
291
|
+
To upgrade, run:
|
292
|
+
|
293
|
+
```sh
|
294
|
+
bundle update blazer
|
295
|
+
```
|
296
|
+
|
297
|
+
Create a migration
|
298
|
+
|
299
|
+
```sh
|
300
|
+
rails g migration upgrade_blazer_to_1_0
|
301
|
+
```
|
302
|
+
|
303
|
+
with:
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
add_column :blazer_queries, :data_source, :string
|
307
|
+
add_column :blazer_audits, :data_source, :string
|
308
|
+
|
309
|
+
create_table :blazer_dashboards do |t|
|
310
|
+
t.text :name
|
311
|
+
t.timestamps
|
312
|
+
end
|
313
|
+
|
314
|
+
create_table :blazer_dashboard_queries do |t|
|
315
|
+
t.references :dashboard
|
316
|
+
t.references :query
|
317
|
+
t.integer :position
|
318
|
+
t.timestamps
|
319
|
+
end
|
320
|
+
|
321
|
+
create_table :blazer_checks do |t|
|
322
|
+
t.references :query
|
323
|
+
t.string :state
|
324
|
+
t.text :emails
|
325
|
+
t.timestamps
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
And run:
|
330
|
+
|
331
|
+
```sh
|
332
|
+
rake db:migrate
|
333
|
+
```
|
334
|
+
|
335
|
+
Update `config/blazer.yml` with:
|
336
|
+
|
337
|
+
```yml
|
338
|
+
# see https://github.com/ankane/blazer for more info
|
339
|
+
|
340
|
+
data_sources:
|
341
|
+
main:
|
342
|
+
url: <%= ENV["BLAZER_DATABASE_URL"] %>
|
343
|
+
|
344
|
+
# timeout: 15 # applies to PostgreSQL only
|
345
|
+
|
346
|
+
smart_variables:
|
347
|
+
# zone_id: "SELECT id, name FROM zones ORDER BY name ASC"
|
348
|
+
|
349
|
+
linked_columns:
|
350
|
+
# user_id: "/admin/users/{value}"
|
351
|
+
|
352
|
+
smart_columns:
|
353
|
+
# user_id: "SELECT id, name FROM users WHERE id IN {value}"
|
354
|
+
|
355
|
+
# create audits
|
356
|
+
audit: true
|
357
|
+
|
358
|
+
# change the time zone
|
359
|
+
# time_zone: "Pacific Time (US & Canada)"
|
360
|
+
|
361
|
+
# class name of the user model
|
362
|
+
# user_class: User
|
363
|
+
|
364
|
+
# method name for the user model
|
365
|
+
# user_name: name
|
366
|
+
```
|
147
367
|
|
148
368
|
## TODO
|
149
369
|
|
150
|
-
- better readme
|
151
|
-
- better navigation
|
152
|
-
- standalone version
|
153
|
-
- update lock
|
154
|
-
- warn when database user has write permissions
|
155
370
|
- advanced permissions
|
156
|
-
-
|
157
|
-
-
|
158
|
-
- support for multiple data sources
|
371
|
+
- standalone version
|
372
|
+
- better navigation
|
159
373
|
|
160
374
|
## History
|
161
375
|
|
@@ -176,6 +390,8 @@ Blazer uses a number of awesome, open source projects.
|
|
176
390
|
|
177
391
|
Created by [ankane](https://github.com/ankane) and [righi](https://github.com/righi)
|
178
392
|
|
393
|
+
Demo data from [MovieLens](http://grouplens.org/datasets/movielens/).
|
394
|
+
|
179
395
|
## Contributing
|
180
396
|
|
181
397
|
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
@@ -0,0 +1,1144 @@
|
|
1
|
+
/**!
|
2
|
+
* Sortable
|
3
|
+
* @author RubaXa <trash@rubaxa.org>
|
4
|
+
* @license MIT
|
5
|
+
*/
|
6
|
+
|
7
|
+
|
8
|
+
(function (factory) {
|
9
|
+
"use strict";
|
10
|
+
|
11
|
+
if (typeof define === "function" && define.amd) {
|
12
|
+
define(factory);
|
13
|
+
}
|
14
|
+
else if (typeof module != "undefined" && typeof module.exports != "undefined") {
|
15
|
+
module.exports = factory();
|
16
|
+
}
|
17
|
+
else if (typeof Package !== "undefined") {
|
18
|
+
Sortable = factory(); // export for Meteor.js
|
19
|
+
}
|
20
|
+
else {
|
21
|
+
/* jshint sub:true */
|
22
|
+
window["Sortable"] = factory();
|
23
|
+
}
|
24
|
+
})(function () {
|
25
|
+
"use strict";
|
26
|
+
|
27
|
+
var dragEl,
|
28
|
+
ghostEl,
|
29
|
+
cloneEl,
|
30
|
+
rootEl,
|
31
|
+
nextEl,
|
32
|
+
|
33
|
+
scrollEl,
|
34
|
+
scrollParentEl,
|
35
|
+
|
36
|
+
lastEl,
|
37
|
+
lastCSS,
|
38
|
+
|
39
|
+
oldIndex,
|
40
|
+
newIndex,
|
41
|
+
|
42
|
+
activeGroup,
|
43
|
+
autoScroll = {},
|
44
|
+
|
45
|
+
tapEvt,
|
46
|
+
touchEvt,
|
47
|
+
|
48
|
+
/** @const */
|
49
|
+
RSPACE = /\s+/g,
|
50
|
+
|
51
|
+
expando = 'Sortable' + (new Date).getTime(),
|
52
|
+
|
53
|
+
win = window,
|
54
|
+
document = win.document,
|
55
|
+
parseInt = win.parseInt,
|
56
|
+
|
57
|
+
supportDraggable = !!('draggable' in document.createElement('div')),
|
58
|
+
|
59
|
+
_silent = false,
|
60
|
+
|
61
|
+
abs = Math.abs,
|
62
|
+
slice = [].slice,
|
63
|
+
|
64
|
+
touchDragOverListeners = [],
|
65
|
+
|
66
|
+
_autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) {
|
67
|
+
// Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
|
68
|
+
if (rootEl && options.scroll) {
|
69
|
+
var el,
|
70
|
+
rect,
|
71
|
+
sens = options.scrollSensitivity,
|
72
|
+
speed = options.scrollSpeed,
|
73
|
+
|
74
|
+
x = evt.clientX,
|
75
|
+
y = evt.clientY,
|
76
|
+
|
77
|
+
winWidth = window.innerWidth,
|
78
|
+
winHeight = window.innerHeight,
|
79
|
+
|
80
|
+
vx,
|
81
|
+
vy
|
82
|
+
;
|
83
|
+
|
84
|
+
// Delect scrollEl
|
85
|
+
if (scrollParentEl !== rootEl) {
|
86
|
+
scrollEl = options.scroll;
|
87
|
+
scrollParentEl = rootEl;
|
88
|
+
|
89
|
+
if (scrollEl === true) {
|
90
|
+
scrollEl = rootEl;
|
91
|
+
|
92
|
+
do {
|
93
|
+
if ((scrollEl.offsetWidth < scrollEl.scrollWidth) ||
|
94
|
+
(scrollEl.offsetHeight < scrollEl.scrollHeight)
|
95
|
+
) {
|
96
|
+
break;
|
97
|
+
}
|
98
|
+
/* jshint boss:true */
|
99
|
+
} while (scrollEl = scrollEl.parentNode);
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
if (scrollEl) {
|
104
|
+
el = scrollEl;
|
105
|
+
rect = scrollEl.getBoundingClientRect();
|
106
|
+
vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens);
|
107
|
+
vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens);
|
108
|
+
}
|
109
|
+
|
110
|
+
|
111
|
+
if (!(vx || vy)) {
|
112
|
+
vx = (winWidth - x <= sens) - (x <= sens);
|
113
|
+
vy = (winHeight - y <= sens) - (y <= sens);
|
114
|
+
|
115
|
+
/* jshint expr:true */
|
116
|
+
(vx || vy) && (el = win);
|
117
|
+
}
|
118
|
+
|
119
|
+
|
120
|
+
if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) {
|
121
|
+
autoScroll.el = el;
|
122
|
+
autoScroll.vx = vx;
|
123
|
+
autoScroll.vy = vy;
|
124
|
+
|
125
|
+
clearInterval(autoScroll.pid);
|
126
|
+
|
127
|
+
if (el) {
|
128
|
+
autoScroll.pid = setInterval(function () {
|
129
|
+
if (el === win) {
|
130
|
+
win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed);
|
131
|
+
} else {
|
132
|
+
vy && (el.scrollTop += vy * speed);
|
133
|
+
vx && (el.scrollLeft += vx * speed);
|
134
|
+
}
|
135
|
+
}, 24);
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}, 30)
|
140
|
+
;
|
141
|
+
|
142
|
+
|
143
|
+
|
144
|
+
/**
|
145
|
+
* @class Sortable
|
146
|
+
* @param {HTMLElement} el
|
147
|
+
* @param {Object} [options]
|
148
|
+
*/
|
149
|
+
function Sortable(el, options) {
|
150
|
+
this.el = el; // root element
|
151
|
+
this.options = options = _extend({}, options);
|
152
|
+
|
153
|
+
|
154
|
+
// Export instance
|
155
|
+
el[expando] = this;
|
156
|
+
|
157
|
+
|
158
|
+
// Default options
|
159
|
+
var defaults = {
|
160
|
+
group: Math.random(),
|
161
|
+
sort: true,
|
162
|
+
disabled: false,
|
163
|
+
store: null,
|
164
|
+
handle: null,
|
165
|
+
scroll: true,
|
166
|
+
scrollSensitivity: 30,
|
167
|
+
scrollSpeed: 10,
|
168
|
+
draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',
|
169
|
+
ghostClass: 'sortable-ghost',
|
170
|
+
ignore: 'a, img',
|
171
|
+
filter: null,
|
172
|
+
animation: 0,
|
173
|
+
setData: function (dataTransfer, dragEl) {
|
174
|
+
dataTransfer.setData('Text', dragEl.textContent);
|
175
|
+
},
|
176
|
+
dropBubble: false,
|
177
|
+
dragoverBubble: false,
|
178
|
+
dataIdAttr: 'data-id',
|
179
|
+
delay: 0
|
180
|
+
};
|
181
|
+
|
182
|
+
|
183
|
+
// Set default options
|
184
|
+
for (var name in defaults) {
|
185
|
+
!(name in options) && (options[name] = defaults[name]);
|
186
|
+
}
|
187
|
+
|
188
|
+
|
189
|
+
var group = options.group;
|
190
|
+
|
191
|
+
if (!group || typeof group != 'object') {
|
192
|
+
group = options.group = { name: group };
|
193
|
+
}
|
194
|
+
|
195
|
+
|
196
|
+
['pull', 'put'].forEach(function (key) {
|
197
|
+
if (!(key in group)) {
|
198
|
+
group[key] = true;
|
199
|
+
}
|
200
|
+
});
|
201
|
+
|
202
|
+
|
203
|
+
options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' ';
|
204
|
+
|
205
|
+
|
206
|
+
// Bind all private methods
|
207
|
+
for (var fn in this) {
|
208
|
+
if (fn.charAt(0) === '_') {
|
209
|
+
this[fn] = _bind(this, this[fn]);
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
|
214
|
+
// Bind events
|
215
|
+
_on(el, 'mousedown', this._onTapStart);
|
216
|
+
_on(el, 'touchstart', this._onTapStart);
|
217
|
+
|
218
|
+
_on(el, 'dragover', this);
|
219
|
+
_on(el, 'dragenter', this);
|
220
|
+
|
221
|
+
touchDragOverListeners.push(this._onDragOver);
|
222
|
+
|
223
|
+
// Restore sorting
|
224
|
+
options.store && this.sort(options.store.get(this));
|
225
|
+
}
|
226
|
+
|
227
|
+
|
228
|
+
Sortable.prototype = /** @lends Sortable.prototype */ {
|
229
|
+
constructor: Sortable,
|
230
|
+
|
231
|
+
_onTapStart: function (/** Event|TouchEvent */evt) {
|
232
|
+
var _this = this,
|
233
|
+
el = this.el,
|
234
|
+
options = this.options,
|
235
|
+
type = evt.type,
|
236
|
+
touch = evt.touches && evt.touches[0],
|
237
|
+
target = (touch || evt).target,
|
238
|
+
originalTarget = target,
|
239
|
+
filter = options.filter;
|
240
|
+
|
241
|
+
|
242
|
+
if (type === 'mousedown' && evt.button !== 0 || options.disabled) {
|
243
|
+
return; // only left button or enabled
|
244
|
+
}
|
245
|
+
|
246
|
+
target = _closest(target, options.draggable, el);
|
247
|
+
|
248
|
+
if (!target) {
|
249
|
+
return;
|
250
|
+
}
|
251
|
+
|
252
|
+
// get the index of the dragged element within its parent
|
253
|
+
oldIndex = _index(target);
|
254
|
+
|
255
|
+
// Check filter
|
256
|
+
if (typeof filter === 'function') {
|
257
|
+
if (filter.call(this, evt, target, this)) {
|
258
|
+
_dispatchEvent(_this, originalTarget, 'filter', target, el, oldIndex);
|
259
|
+
evt.preventDefault();
|
260
|
+
return; // cancel dnd
|
261
|
+
}
|
262
|
+
}
|
263
|
+
else if (filter) {
|
264
|
+
filter = filter.split(',').some(function (criteria) {
|
265
|
+
criteria = _closest(originalTarget, criteria.trim(), el);
|
266
|
+
|
267
|
+
if (criteria) {
|
268
|
+
_dispatchEvent(_this, criteria, 'filter', target, el, oldIndex);
|
269
|
+
return true;
|
270
|
+
}
|
271
|
+
});
|
272
|
+
|
273
|
+
if (filter) {
|
274
|
+
evt.preventDefault();
|
275
|
+
return; // cancel dnd
|
276
|
+
}
|
277
|
+
}
|
278
|
+
|
279
|
+
|
280
|
+
if (options.handle && !_closest(originalTarget, options.handle, el)) {
|
281
|
+
return;
|
282
|
+
}
|
283
|
+
|
284
|
+
|
285
|
+
// Prepare `dragstart`
|
286
|
+
this._prepareDragStart(evt, touch, target);
|
287
|
+
},
|
288
|
+
|
289
|
+
_prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target) {
|
290
|
+
var _this = this,
|
291
|
+
el = _this.el,
|
292
|
+
options = _this.options,
|
293
|
+
ownerDocument = el.ownerDocument,
|
294
|
+
dragStartFn;
|
295
|
+
|
296
|
+
if (target && !dragEl && (target.parentNode === el)) {
|
297
|
+
tapEvt = evt;
|
298
|
+
|
299
|
+
rootEl = el;
|
300
|
+
dragEl = target;
|
301
|
+
nextEl = dragEl.nextSibling;
|
302
|
+
activeGroup = options.group;
|
303
|
+
|
304
|
+
dragStartFn = function () {
|
305
|
+
// Delayed drag has been triggered
|
306
|
+
// we can re-enable the events: touchmove/mousemove
|
307
|
+
_this._disableDelayedDrag();
|
308
|
+
|
309
|
+
// Make the element draggable
|
310
|
+
dragEl.draggable = true;
|
311
|
+
|
312
|
+
// Disable "draggable"
|
313
|
+
options.ignore.split(',').forEach(function (criteria) {
|
314
|
+
_find(dragEl, criteria.trim(), _disableDraggable);
|
315
|
+
});
|
316
|
+
|
317
|
+
// Bind the events: dragstart/dragend
|
318
|
+
_this._triggerDragStart(touch);
|
319
|
+
};
|
320
|
+
|
321
|
+
_on(ownerDocument, 'mouseup', _this._onDrop);
|
322
|
+
_on(ownerDocument, 'touchend', _this._onDrop);
|
323
|
+
_on(ownerDocument, 'touchcancel', _this._onDrop);
|
324
|
+
|
325
|
+
if (options.delay) {
|
326
|
+
// If the user moves the pointer before the delay has been reached:
|
327
|
+
// disable the delayed drag
|
328
|
+
_on(ownerDocument, 'mousemove', _this._disableDelayedDrag);
|
329
|
+
_on(ownerDocument, 'touchmove', _this._disableDelayedDrag);
|
330
|
+
|
331
|
+
_this._dragStartTimer = setTimeout(dragStartFn, options.delay);
|
332
|
+
} else {
|
333
|
+
dragStartFn();
|
334
|
+
}
|
335
|
+
}
|
336
|
+
},
|
337
|
+
|
338
|
+
_disableDelayedDrag: function () {
|
339
|
+
var ownerDocument = this.el.ownerDocument;
|
340
|
+
|
341
|
+
clearTimeout(this._dragStartTimer);
|
342
|
+
|
343
|
+
_off(ownerDocument, 'mousemove', this._disableDelayedDrag);
|
344
|
+
_off(ownerDocument, 'touchmove', this._disableDelayedDrag);
|
345
|
+
},
|
346
|
+
|
347
|
+
_triggerDragStart: function (/** Touch */touch) {
|
348
|
+
if (touch) {
|
349
|
+
// Touch device support
|
350
|
+
tapEvt = {
|
351
|
+
target: dragEl,
|
352
|
+
clientX: touch.clientX,
|
353
|
+
clientY: touch.clientY
|
354
|
+
};
|
355
|
+
|
356
|
+
this._onDragStart(tapEvt, 'touch');
|
357
|
+
}
|
358
|
+
else if (!supportDraggable) {
|
359
|
+
this._onDragStart(tapEvt, true);
|
360
|
+
}
|
361
|
+
else {
|
362
|
+
_on(dragEl, 'dragend', this);
|
363
|
+
_on(rootEl, 'dragstart', this._onDragStart);
|
364
|
+
}
|
365
|
+
|
366
|
+
try {
|
367
|
+
if (document.selection) {
|
368
|
+
document.selection.empty();
|
369
|
+
} else {
|
370
|
+
window.getSelection().removeAllRanges();
|
371
|
+
}
|
372
|
+
} catch (err) {
|
373
|
+
}
|
374
|
+
},
|
375
|
+
|
376
|
+
_dragStarted: function () {
|
377
|
+
if (rootEl && dragEl) {
|
378
|
+
// Apply effect
|
379
|
+
_toggleClass(dragEl, this.options.ghostClass, true);
|
380
|
+
|
381
|
+
Sortable.active = this;
|
382
|
+
|
383
|
+
// Drag start event
|
384
|
+
_dispatchEvent(this, rootEl, 'start', dragEl, rootEl, oldIndex);
|
385
|
+
}
|
386
|
+
},
|
387
|
+
|
388
|
+
_emulateDragOver: function () {
|
389
|
+
if (touchEvt) {
|
390
|
+
_css(ghostEl, 'display', 'none');
|
391
|
+
|
392
|
+
var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
|
393
|
+
parent = target,
|
394
|
+
groupName = ' ' + this.options.group.name + '',
|
395
|
+
i = touchDragOverListeners.length;
|
396
|
+
|
397
|
+
if (parent) {
|
398
|
+
do {
|
399
|
+
if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) {
|
400
|
+
while (i--) {
|
401
|
+
touchDragOverListeners[i]({
|
402
|
+
clientX: touchEvt.clientX,
|
403
|
+
clientY: touchEvt.clientY,
|
404
|
+
target: target,
|
405
|
+
rootEl: parent
|
406
|
+
});
|
407
|
+
}
|
408
|
+
|
409
|
+
break;
|
410
|
+
}
|
411
|
+
|
412
|
+
target = parent; // store last element
|
413
|
+
}
|
414
|
+
/* jshint boss:true */
|
415
|
+
while (parent = parent.parentNode);
|
416
|
+
}
|
417
|
+
|
418
|
+
_css(ghostEl, 'display', '');
|
419
|
+
}
|
420
|
+
},
|
421
|
+
|
422
|
+
|
423
|
+
_onTouchMove: function (/**TouchEvent*/evt) {
|
424
|
+
if (tapEvt) {
|
425
|
+
var touch = evt.touches ? evt.touches[0] : evt,
|
426
|
+
dx = touch.clientX - tapEvt.clientX,
|
427
|
+
dy = touch.clientY - tapEvt.clientY,
|
428
|
+
translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';
|
429
|
+
|
430
|
+
touchEvt = touch;
|
431
|
+
|
432
|
+
_css(ghostEl, 'webkitTransform', translate3d);
|
433
|
+
_css(ghostEl, 'mozTransform', translate3d);
|
434
|
+
_css(ghostEl, 'msTransform', translate3d);
|
435
|
+
_css(ghostEl, 'transform', translate3d);
|
436
|
+
|
437
|
+
evt.preventDefault();
|
438
|
+
}
|
439
|
+
},
|
440
|
+
|
441
|
+
|
442
|
+
_onDragStart: function (/**Event*/evt, /**boolean*/useFallback) {
|
443
|
+
var dataTransfer = evt.dataTransfer,
|
444
|
+
options = this.options;
|
445
|
+
|
446
|
+
this._offUpEvents();
|
447
|
+
|
448
|
+
if (activeGroup.pull == 'clone') {
|
449
|
+
cloneEl = dragEl.cloneNode(true);
|
450
|
+
_css(cloneEl, 'display', 'none');
|
451
|
+
rootEl.insertBefore(cloneEl, dragEl);
|
452
|
+
}
|
453
|
+
|
454
|
+
if (useFallback) {
|
455
|
+
var rect = dragEl.getBoundingClientRect(),
|
456
|
+
css = _css(dragEl),
|
457
|
+
ghostRect;
|
458
|
+
|
459
|
+
ghostEl = dragEl.cloneNode(true);
|
460
|
+
|
461
|
+
_css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
|
462
|
+
_css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
|
463
|
+
_css(ghostEl, 'width', rect.width);
|
464
|
+
_css(ghostEl, 'height', rect.height);
|
465
|
+
_css(ghostEl, 'opacity', '0.8');
|
466
|
+
_css(ghostEl, 'position', 'fixed');
|
467
|
+
_css(ghostEl, 'zIndex', '100000');
|
468
|
+
|
469
|
+
rootEl.appendChild(ghostEl);
|
470
|
+
|
471
|
+
// Fixing dimensions.
|
472
|
+
ghostRect = ghostEl.getBoundingClientRect();
|
473
|
+
_css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
|
474
|
+
_css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
|
475
|
+
|
476
|
+
if (useFallback === 'touch') {
|
477
|
+
// Bind touch events
|
478
|
+
_on(document, 'touchmove', this._onTouchMove);
|
479
|
+
_on(document, 'touchend', this._onDrop);
|
480
|
+
_on(document, 'touchcancel', this._onDrop);
|
481
|
+
} else {
|
482
|
+
// Old brwoser
|
483
|
+
_on(document, 'mousemove', this._onTouchMove);
|
484
|
+
_on(document, 'mouseup', this._onDrop);
|
485
|
+
}
|
486
|
+
|
487
|
+
this._loopId = setInterval(this._emulateDragOver, 150);
|
488
|
+
}
|
489
|
+
else {
|
490
|
+
if (dataTransfer) {
|
491
|
+
dataTransfer.effectAllowed = 'move';
|
492
|
+
options.setData && options.setData.call(this, dataTransfer, dragEl);
|
493
|
+
}
|
494
|
+
|
495
|
+
_on(document, 'drop', this);
|
496
|
+
}
|
497
|
+
|
498
|
+
setTimeout(this._dragStarted, 0);
|
499
|
+
},
|
500
|
+
|
501
|
+
_onDragOver: function (/**Event*/evt) {
|
502
|
+
var el = this.el,
|
503
|
+
target,
|
504
|
+
dragRect,
|
505
|
+
revert,
|
506
|
+
options = this.options,
|
507
|
+
group = options.group,
|
508
|
+
groupPut = group.put,
|
509
|
+
isOwner = (activeGroup === group),
|
510
|
+
canSort = options.sort;
|
511
|
+
|
512
|
+
if (evt.preventDefault !== void 0) {
|
513
|
+
evt.preventDefault();
|
514
|
+
!options.dragoverBubble && evt.stopPropagation();
|
515
|
+
}
|
516
|
+
|
517
|
+
if (activeGroup && !options.disabled &&
|
518
|
+
(isOwner
|
519
|
+
? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
|
520
|
+
: activeGroup.pull && groupPut && (
|
521
|
+
(activeGroup.name === group.name) || // by Name
|
522
|
+
(groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array
|
523
|
+
)
|
524
|
+
) &&
|
525
|
+
(evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback
|
526
|
+
) {
|
527
|
+
// Smart auto-scrolling
|
528
|
+
_autoScroll(evt, options, this.el);
|
529
|
+
|
530
|
+
if (_silent) {
|
531
|
+
return;
|
532
|
+
}
|
533
|
+
|
534
|
+
target = _closest(evt.target, options.draggable, el);
|
535
|
+
dragRect = dragEl.getBoundingClientRect();
|
536
|
+
|
537
|
+
|
538
|
+
if (revert) {
|
539
|
+
_cloneHide(true);
|
540
|
+
|
541
|
+
if (cloneEl || nextEl) {
|
542
|
+
rootEl.insertBefore(dragEl, cloneEl || nextEl);
|
543
|
+
}
|
544
|
+
else if (!canSort) {
|
545
|
+
rootEl.appendChild(dragEl);
|
546
|
+
}
|
547
|
+
|
548
|
+
return;
|
549
|
+
}
|
550
|
+
|
551
|
+
|
552
|
+
if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
|
553
|
+
(el === evt.target) && (target = _ghostInBottom(el, evt))
|
554
|
+
) {
|
555
|
+
if (target) {
|
556
|
+
if (target.animated) {
|
557
|
+
return;
|
558
|
+
}
|
559
|
+
targetRect = target.getBoundingClientRect();
|
560
|
+
}
|
561
|
+
|
562
|
+
_cloneHide(isOwner);
|
563
|
+
|
564
|
+
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect) !== false) {
|
565
|
+
el.appendChild(dragEl);
|
566
|
+
this._animate(dragRect, dragEl);
|
567
|
+
target && this._animate(targetRect, target);
|
568
|
+
}
|
569
|
+
}
|
570
|
+
else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) {
|
571
|
+
if (lastEl !== target) {
|
572
|
+
lastEl = target;
|
573
|
+
lastCSS = _css(target);
|
574
|
+
}
|
575
|
+
|
576
|
+
|
577
|
+
var targetRect = target.getBoundingClientRect(),
|
578
|
+
width = targetRect.right - targetRect.left,
|
579
|
+
height = targetRect.bottom - targetRect.top,
|
580
|
+
floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display),
|
581
|
+
isWide = (target.offsetWidth > dragEl.offsetWidth),
|
582
|
+
isLong = (target.offsetHeight > dragEl.offsetHeight),
|
583
|
+
halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
|
584
|
+
nextSibling = target.nextElementSibling,
|
585
|
+
moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect),
|
586
|
+
after
|
587
|
+
;
|
588
|
+
|
589
|
+
if (moveVector !== false) {
|
590
|
+
_silent = true;
|
591
|
+
setTimeout(_unsilent, 30);
|
592
|
+
|
593
|
+
_cloneHide(isOwner);
|
594
|
+
|
595
|
+
if (moveVector === 1 || moveVector === -1) {
|
596
|
+
after = (moveVector === 1);
|
597
|
+
}
|
598
|
+
else if (floating) {
|
599
|
+
after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
|
600
|
+
} else {
|
601
|
+
after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
|
602
|
+
}
|
603
|
+
|
604
|
+
if (after && !nextSibling) {
|
605
|
+
el.appendChild(dragEl);
|
606
|
+
} else {
|
607
|
+
target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
|
608
|
+
}
|
609
|
+
|
610
|
+
this._animate(dragRect, dragEl);
|
611
|
+
this._animate(targetRect, target);
|
612
|
+
}
|
613
|
+
}
|
614
|
+
}
|
615
|
+
},
|
616
|
+
|
617
|
+
_animate: function (prevRect, target) {
|
618
|
+
var ms = this.options.animation;
|
619
|
+
|
620
|
+
if (ms) {
|
621
|
+
var currentRect = target.getBoundingClientRect();
|
622
|
+
|
623
|
+
_css(target, 'transition', 'none');
|
624
|
+
_css(target, 'transform', 'translate3d('
|
625
|
+
+ (prevRect.left - currentRect.left) + 'px,'
|
626
|
+
+ (prevRect.top - currentRect.top) + 'px,0)'
|
627
|
+
);
|
628
|
+
|
629
|
+
target.offsetWidth; // repaint
|
630
|
+
|
631
|
+
_css(target, 'transition', 'all ' + ms + 'ms');
|
632
|
+
_css(target, 'transform', 'translate3d(0,0,0)');
|
633
|
+
|
634
|
+
clearTimeout(target.animated);
|
635
|
+
target.animated = setTimeout(function () {
|
636
|
+
_css(target, 'transition', '');
|
637
|
+
_css(target, 'transform', '');
|
638
|
+
target.animated = false;
|
639
|
+
}, ms);
|
640
|
+
}
|
641
|
+
},
|
642
|
+
|
643
|
+
_offUpEvents: function () {
|
644
|
+
var ownerDocument = this.el.ownerDocument;
|
645
|
+
|
646
|
+
_off(document, 'touchmove', this._onTouchMove);
|
647
|
+
_off(ownerDocument, 'mouseup', this._onDrop);
|
648
|
+
_off(ownerDocument, 'touchend', this._onDrop);
|
649
|
+
_off(ownerDocument, 'touchcancel', this._onDrop);
|
650
|
+
},
|
651
|
+
|
652
|
+
_onDrop: function (/**Event*/evt) {
|
653
|
+
var el = this.el,
|
654
|
+
options = this.options;
|
655
|
+
|
656
|
+
clearInterval(this._loopId);
|
657
|
+
clearInterval(autoScroll.pid);
|
658
|
+
clearTimeout(this._dragStartTimer);
|
659
|
+
|
660
|
+
// Unbind events
|
661
|
+
_off(document, 'drop', this);
|
662
|
+
_off(document, 'mousemove', this._onTouchMove);
|
663
|
+
_off(el, 'dragstart', this._onDragStart);
|
664
|
+
|
665
|
+
this._offUpEvents();
|
666
|
+
|
667
|
+
if (evt) {
|
668
|
+
evt.preventDefault();
|
669
|
+
!options.dropBubble && evt.stopPropagation();
|
670
|
+
|
671
|
+
ghostEl && ghostEl.parentNode.removeChild(ghostEl);
|
672
|
+
|
673
|
+
if (dragEl) {
|
674
|
+
_off(dragEl, 'dragend', this);
|
675
|
+
|
676
|
+
_disableDraggable(dragEl);
|
677
|
+
_toggleClass(dragEl, this.options.ghostClass, false);
|
678
|
+
|
679
|
+
if (rootEl !== dragEl.parentNode) {
|
680
|
+
newIndex = _index(dragEl);
|
681
|
+
|
682
|
+
// drag from one list and drop into another
|
683
|
+
_dispatchEvent(null, dragEl.parentNode, 'sort', dragEl, rootEl, oldIndex, newIndex);
|
684
|
+
_dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
|
685
|
+
|
686
|
+
// Add event
|
687
|
+
_dispatchEvent(null, dragEl.parentNode, 'add', dragEl, rootEl, oldIndex, newIndex);
|
688
|
+
|
689
|
+
// Remove event
|
690
|
+
_dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex);
|
691
|
+
}
|
692
|
+
else {
|
693
|
+
// Remove clone
|
694
|
+
cloneEl && cloneEl.parentNode.removeChild(cloneEl);
|
695
|
+
|
696
|
+
if (dragEl.nextSibling !== nextEl) {
|
697
|
+
// Get the index of the dragged element within its parent
|
698
|
+
newIndex = _index(dragEl);
|
699
|
+
|
700
|
+
// drag & drop within the same list
|
701
|
+
_dispatchEvent(this, rootEl, 'update', dragEl, rootEl, oldIndex, newIndex);
|
702
|
+
_dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
|
703
|
+
}
|
704
|
+
}
|
705
|
+
|
706
|
+
if (Sortable.active) {
|
707
|
+
// Drag end event
|
708
|
+
_dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex);
|
709
|
+
|
710
|
+
// Save sorting
|
711
|
+
this.save();
|
712
|
+
}
|
713
|
+
}
|
714
|
+
|
715
|
+
// Nulling
|
716
|
+
rootEl =
|
717
|
+
dragEl =
|
718
|
+
ghostEl =
|
719
|
+
nextEl =
|
720
|
+
cloneEl =
|
721
|
+
|
722
|
+
scrollEl =
|
723
|
+
scrollParentEl =
|
724
|
+
|
725
|
+
tapEvt =
|
726
|
+
touchEvt =
|
727
|
+
|
728
|
+
lastEl =
|
729
|
+
lastCSS =
|
730
|
+
|
731
|
+
activeGroup =
|
732
|
+
Sortable.active = null;
|
733
|
+
}
|
734
|
+
},
|
735
|
+
|
736
|
+
|
737
|
+
handleEvent: function (/**Event*/evt) {
|
738
|
+
var type = evt.type;
|
739
|
+
|
740
|
+
if (type === 'dragover' || type === 'dragenter') {
|
741
|
+
if (dragEl) {
|
742
|
+
this._onDragOver(evt);
|
743
|
+
_globalDragOver(evt);
|
744
|
+
}
|
745
|
+
}
|
746
|
+
else if (type === 'drop' || type === 'dragend') {
|
747
|
+
this._onDrop(evt);
|
748
|
+
}
|
749
|
+
},
|
750
|
+
|
751
|
+
|
752
|
+
/**
|
753
|
+
* Serializes the item into an array of string.
|
754
|
+
* @returns {String[]}
|
755
|
+
*/
|
756
|
+
toArray: function () {
|
757
|
+
var order = [],
|
758
|
+
el,
|
759
|
+
children = this.el.children,
|
760
|
+
i = 0,
|
761
|
+
n = children.length,
|
762
|
+
options = this.options;
|
763
|
+
|
764
|
+
for (; i < n; i++) {
|
765
|
+
el = children[i];
|
766
|
+
if (_closest(el, options.draggable, this.el)) {
|
767
|
+
order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
|
768
|
+
}
|
769
|
+
}
|
770
|
+
|
771
|
+
return order;
|
772
|
+
},
|
773
|
+
|
774
|
+
|
775
|
+
/**
|
776
|
+
* Sorts the elements according to the array.
|
777
|
+
* @param {String[]} order order of the items
|
778
|
+
*/
|
779
|
+
sort: function (order) {
|
780
|
+
var items = {}, rootEl = this.el;
|
781
|
+
|
782
|
+
this.toArray().forEach(function (id, i) {
|
783
|
+
var el = rootEl.children[i];
|
784
|
+
|
785
|
+
if (_closest(el, this.options.draggable, rootEl)) {
|
786
|
+
items[id] = el;
|
787
|
+
}
|
788
|
+
}, this);
|
789
|
+
|
790
|
+
order.forEach(function (id) {
|
791
|
+
if (items[id]) {
|
792
|
+
rootEl.removeChild(items[id]);
|
793
|
+
rootEl.appendChild(items[id]);
|
794
|
+
}
|
795
|
+
});
|
796
|
+
},
|
797
|
+
|
798
|
+
|
799
|
+
/**
|
800
|
+
* Save the current sorting
|
801
|
+
*/
|
802
|
+
save: function () {
|
803
|
+
var store = this.options.store;
|
804
|
+
store && store.set(this);
|
805
|
+
},
|
806
|
+
|
807
|
+
|
808
|
+
/**
|
809
|
+
* For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
|
810
|
+
* @param {HTMLElement} el
|
811
|
+
* @param {String} [selector] default: `options.draggable`
|
812
|
+
* @returns {HTMLElement|null}
|
813
|
+
*/
|
814
|
+
closest: function (el, selector) {
|
815
|
+
return _closest(el, selector || this.options.draggable, this.el);
|
816
|
+
},
|
817
|
+
|
818
|
+
|
819
|
+
/**
|
820
|
+
* Set/get option
|
821
|
+
* @param {string} name
|
822
|
+
* @param {*} [value]
|
823
|
+
* @returns {*}
|
824
|
+
*/
|
825
|
+
option: function (name, value) {
|
826
|
+
var options = this.options;
|
827
|
+
|
828
|
+
if (value === void 0) {
|
829
|
+
return options[name];
|
830
|
+
} else {
|
831
|
+
options[name] = value;
|
832
|
+
}
|
833
|
+
},
|
834
|
+
|
835
|
+
|
836
|
+
/**
|
837
|
+
* Destroy
|
838
|
+
*/
|
839
|
+
destroy: function () {
|
840
|
+
var el = this.el;
|
841
|
+
|
842
|
+
el[expando] = null;
|
843
|
+
|
844
|
+
_off(el, 'mousedown', this._onTapStart);
|
845
|
+
_off(el, 'touchstart', this._onTapStart);
|
846
|
+
|
847
|
+
_off(el, 'dragover', this);
|
848
|
+
_off(el, 'dragenter', this);
|
849
|
+
|
850
|
+
// Remove draggable attributes
|
851
|
+
Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
|
852
|
+
el.removeAttribute('draggable');
|
853
|
+
});
|
854
|
+
|
855
|
+
touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);
|
856
|
+
|
857
|
+
this._onDrop();
|
858
|
+
|
859
|
+
this.el = el = null;
|
860
|
+
}
|
861
|
+
};
|
862
|
+
|
863
|
+
|
864
|
+
function _cloneHide(state) {
|
865
|
+
if (cloneEl && (cloneEl.state !== state)) {
|
866
|
+
_css(cloneEl, 'display', state ? 'none' : '');
|
867
|
+
!state && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl);
|
868
|
+
cloneEl.state = state;
|
869
|
+
}
|
870
|
+
}
|
871
|
+
|
872
|
+
|
873
|
+
function _bind(ctx, fn) {
|
874
|
+
var args = slice.call(arguments, 2);
|
875
|
+
return fn.bind ? fn.bind.apply(fn, [ctx].concat(args)) : function () {
|
876
|
+
return fn.apply(ctx, args.concat(slice.call(arguments)));
|
877
|
+
};
|
878
|
+
}
|
879
|
+
|
880
|
+
|
881
|
+
function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) {
|
882
|
+
if (el) {
|
883
|
+
ctx = ctx || document;
|
884
|
+
selector = selector.split('.');
|
885
|
+
|
886
|
+
var tag = selector.shift().toUpperCase(),
|
887
|
+
re = new RegExp('\\s(' + selector.join('|') + ')(?=\\s)', 'g');
|
888
|
+
|
889
|
+
do {
|
890
|
+
if (
|
891
|
+
(tag === '>*' && el.parentNode === ctx) || (
|
892
|
+
(tag === '' || el.nodeName.toUpperCase() == tag) &&
|
893
|
+
(!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length)
|
894
|
+
)
|
895
|
+
) {
|
896
|
+
return el;
|
897
|
+
}
|
898
|
+
}
|
899
|
+
while (el !== ctx && (el = el.parentNode));
|
900
|
+
}
|
901
|
+
|
902
|
+
return null;
|
903
|
+
}
|
904
|
+
|
905
|
+
|
906
|
+
function _globalDragOver(/**Event*/evt) {
|
907
|
+
evt.dataTransfer.dropEffect = 'move';
|
908
|
+
evt.preventDefault();
|
909
|
+
}
|
910
|
+
|
911
|
+
|
912
|
+
function _on(el, event, fn) {
|
913
|
+
el.addEventListener(event, fn, false);
|
914
|
+
}
|
915
|
+
|
916
|
+
|
917
|
+
function _off(el, event, fn) {
|
918
|
+
el.removeEventListener(event, fn, false);
|
919
|
+
}
|
920
|
+
|
921
|
+
|
922
|
+
function _toggleClass(el, name, state) {
|
923
|
+
if (el) {
|
924
|
+
if (el.classList) {
|
925
|
+
el.classList[state ? 'add' : 'remove'](name);
|
926
|
+
}
|
927
|
+
else {
|
928
|
+
var className = (' ' + el.className + ' ').replace(RSPACE, ' ').replace(' ' + name + ' ', ' ');
|
929
|
+
el.className = (className + (state ? ' ' + name : '')).replace(RSPACE, ' ');
|
930
|
+
}
|
931
|
+
}
|
932
|
+
}
|
933
|
+
|
934
|
+
|
935
|
+
function _css(el, prop, val) {
|
936
|
+
var style = el && el.style;
|
937
|
+
|
938
|
+
if (style) {
|
939
|
+
if (val === void 0) {
|
940
|
+
if (document.defaultView && document.defaultView.getComputedStyle) {
|
941
|
+
val = document.defaultView.getComputedStyle(el, '');
|
942
|
+
}
|
943
|
+
else if (el.currentStyle) {
|
944
|
+
val = el.currentStyle;
|
945
|
+
}
|
946
|
+
|
947
|
+
return prop === void 0 ? val : val[prop];
|
948
|
+
}
|
949
|
+
else {
|
950
|
+
if (!(prop in style)) {
|
951
|
+
prop = '-webkit-' + prop;
|
952
|
+
}
|
953
|
+
|
954
|
+
style[prop] = val + (typeof val === 'string' ? '' : 'px');
|
955
|
+
}
|
956
|
+
}
|
957
|
+
}
|
958
|
+
|
959
|
+
|
960
|
+
function _find(ctx, tagName, iterator) {
|
961
|
+
if (ctx) {
|
962
|
+
var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
|
963
|
+
|
964
|
+
if (iterator) {
|
965
|
+
for (; i < n; i++) {
|
966
|
+
iterator(list[i], i);
|
967
|
+
}
|
968
|
+
}
|
969
|
+
|
970
|
+
return list;
|
971
|
+
}
|
972
|
+
|
973
|
+
return [];
|
974
|
+
}
|
975
|
+
|
976
|
+
|
977
|
+
|
978
|
+
function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) {
|
979
|
+
var evt = document.createEvent('Event'),
|
980
|
+
options = (sortable || rootEl[expando]).options,
|
981
|
+
onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);
|
982
|
+
|
983
|
+
evt.initEvent(name, true, true);
|
984
|
+
|
985
|
+
evt.to = rootEl;
|
986
|
+
evt.from = fromEl || rootEl;
|
987
|
+
evt.item = targetEl || rootEl;
|
988
|
+
evt.clone = cloneEl;
|
989
|
+
|
990
|
+
evt.oldIndex = startIndex;
|
991
|
+
evt.newIndex = newIndex;
|
992
|
+
|
993
|
+
rootEl.dispatchEvent(evt);
|
994
|
+
|
995
|
+
if (options[onName]) {
|
996
|
+
options[onName].call(sortable, evt);
|
997
|
+
}
|
998
|
+
}
|
999
|
+
|
1000
|
+
|
1001
|
+
function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect) {
|
1002
|
+
var evt,
|
1003
|
+
sortable = fromEl[expando],
|
1004
|
+
onMoveFn = sortable.options.onMove,
|
1005
|
+
retVal;
|
1006
|
+
|
1007
|
+
if (onMoveFn) {
|
1008
|
+
evt = document.createEvent('Event');
|
1009
|
+
evt.initEvent('move', true, true);
|
1010
|
+
|
1011
|
+
evt.to = toEl;
|
1012
|
+
evt.from = fromEl;
|
1013
|
+
evt.dragged = dragEl;
|
1014
|
+
evt.draggedRect = dragRect;
|
1015
|
+
evt.related = targetEl || toEl;
|
1016
|
+
evt.relatedRect = targetRect || toEl.getBoundingClientRect();
|
1017
|
+
|
1018
|
+
retVal = onMoveFn.call(sortable, evt);
|
1019
|
+
}
|
1020
|
+
|
1021
|
+
return retVal;
|
1022
|
+
}
|
1023
|
+
|
1024
|
+
|
1025
|
+
function _disableDraggable(el) {
|
1026
|
+
el.draggable = false;
|
1027
|
+
}
|
1028
|
+
|
1029
|
+
|
1030
|
+
function _unsilent() {
|
1031
|
+
_silent = false;
|
1032
|
+
}
|
1033
|
+
|
1034
|
+
|
1035
|
+
/** @returns {HTMLElement|false} */
|
1036
|
+
function _ghostInBottom(el, evt) {
|
1037
|
+
var lastEl = el.lastElementChild,
|
1038
|
+
rect = lastEl.getBoundingClientRect();
|
1039
|
+
|
1040
|
+
return (evt.clientY - (rect.top + rect.height) > 5) && lastEl; // min delta
|
1041
|
+
}
|
1042
|
+
|
1043
|
+
|
1044
|
+
/**
|
1045
|
+
* Generate id
|
1046
|
+
* @param {HTMLElement} el
|
1047
|
+
* @returns {String}
|
1048
|
+
* @private
|
1049
|
+
*/
|
1050
|
+
function _generateId(el) {
|
1051
|
+
var str = el.tagName + el.className + el.src + el.href + el.textContent,
|
1052
|
+
i = str.length,
|
1053
|
+
sum = 0;
|
1054
|
+
|
1055
|
+
while (i--) {
|
1056
|
+
sum += str.charCodeAt(i);
|
1057
|
+
}
|
1058
|
+
|
1059
|
+
return sum.toString(36);
|
1060
|
+
}
|
1061
|
+
|
1062
|
+
/**
|
1063
|
+
* Returns the index of an element within its parent
|
1064
|
+
* @param el
|
1065
|
+
* @returns {number}
|
1066
|
+
* @private
|
1067
|
+
*/
|
1068
|
+
function _index(/**HTMLElement*/el) {
|
1069
|
+
var index = 0;
|
1070
|
+
while (el && (el = el.previousElementSibling)) {
|
1071
|
+
if (el.nodeName.toUpperCase() !== 'TEMPLATE') {
|
1072
|
+
index++;
|
1073
|
+
}
|
1074
|
+
}
|
1075
|
+
return index;
|
1076
|
+
}
|
1077
|
+
|
1078
|
+
function _throttle(callback, ms) {
|
1079
|
+
var args, _this;
|
1080
|
+
|
1081
|
+
return function () {
|
1082
|
+
if (args === void 0) {
|
1083
|
+
args = arguments;
|
1084
|
+
_this = this;
|
1085
|
+
|
1086
|
+
setTimeout(function () {
|
1087
|
+
if (args.length === 1) {
|
1088
|
+
callback.call(_this, args[0]);
|
1089
|
+
} else {
|
1090
|
+
callback.apply(_this, args);
|
1091
|
+
}
|
1092
|
+
|
1093
|
+
args = void 0;
|
1094
|
+
}, ms);
|
1095
|
+
}
|
1096
|
+
};
|
1097
|
+
}
|
1098
|
+
|
1099
|
+
function _extend(dst, src) {
|
1100
|
+
if (dst && src) {
|
1101
|
+
for (var key in src) {
|
1102
|
+
if (src.hasOwnProperty(key)) {
|
1103
|
+
dst[key] = src[key];
|
1104
|
+
}
|
1105
|
+
}
|
1106
|
+
}
|
1107
|
+
|
1108
|
+
return dst;
|
1109
|
+
}
|
1110
|
+
|
1111
|
+
|
1112
|
+
// Export utils
|
1113
|
+
Sortable.utils = {
|
1114
|
+
on: _on,
|
1115
|
+
off: _off,
|
1116
|
+
css: _css,
|
1117
|
+
find: _find,
|
1118
|
+
bind: _bind,
|
1119
|
+
is: function (el, selector) {
|
1120
|
+
return !!_closest(el, selector, el);
|
1121
|
+
},
|
1122
|
+
extend: _extend,
|
1123
|
+
throttle: _throttle,
|
1124
|
+
closest: _closest,
|
1125
|
+
toggleClass: _toggleClass,
|
1126
|
+
index: _index
|
1127
|
+
};
|
1128
|
+
|
1129
|
+
|
1130
|
+
Sortable.version = '1.2.1';
|
1131
|
+
|
1132
|
+
|
1133
|
+
/**
|
1134
|
+
* Create sortable instance
|
1135
|
+
* @param {HTMLElement} el
|
1136
|
+
* @param {Object} [options]
|
1137
|
+
*/
|
1138
|
+
Sortable.create = function (el, options) {
|
1139
|
+
return new Sortable(el, options);
|
1140
|
+
};
|
1141
|
+
|
1142
|
+
// Export
|
1143
|
+
return Sortable;
|
1144
|
+
});
|