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.

Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +261 -45
  4. data/app/assets/javascripts/blazer/Sortable.js +1144 -0
  5. data/app/assets/javascripts/blazer/application.js +2 -1
  6. data/app/assets/javascripts/blazer/chartkick.js +935 -0
  7. data/app/assets/javascripts/blazer/selectize.js +391 -201
  8. data/app/assets/stylesheets/blazer/application.css +17 -2
  9. data/app/assets/stylesheets/blazer/selectize.default.css +3 -2
  10. data/app/controllers/blazer/base_controller.rb +48 -0
  11. data/app/controllers/blazer/checks_controller.rb +51 -0
  12. data/app/controllers/blazer/dashboards_controller.rb +94 -0
  13. data/app/controllers/blazer/queries_controller.rb +29 -101
  14. data/app/helpers/blazer/{queries_helper.rb → base_helper.rb} +1 -1
  15. data/app/mailers/blazer/check_mailer.rb +21 -0
  16. data/app/models/blazer/check.rb +28 -0
  17. data/app/models/blazer/connection.rb +0 -1
  18. data/app/models/blazer/dashboard.rb +12 -0
  19. data/app/models/blazer/dashboard_query.rb +9 -0
  20. data/app/models/blazer/query.rb +5 -0
  21. data/app/views/blazer/check_mailer/failing_checks.html.erb +6 -0
  22. data/app/views/blazer/check_mailer/state_change.html.erb +6 -0
  23. data/app/views/blazer/checks/_form.html.erb +28 -0
  24. data/app/views/blazer/checks/edit.html.erb +1 -0
  25. data/app/views/blazer/checks/index.html.erb +41 -0
  26. data/app/views/blazer/checks/new.html.erb +1 -0
  27. data/app/views/blazer/checks/run.html.erb +9 -0
  28. data/app/views/blazer/dashboards/_form.html.erb +86 -0
  29. data/app/views/blazer/dashboards/edit.html.erb +1 -0
  30. data/app/views/blazer/dashboards/index.html.erb +21 -0
  31. data/app/views/blazer/dashboards/new.html.erb +1 -0
  32. data/app/views/blazer/dashboards/show.html.erb +148 -0
  33. data/app/views/blazer/queries/_form.html.erb +16 -5
  34. data/app/views/blazer/queries/_tables.html +5 -0
  35. data/app/views/blazer/queries/index.html.erb +6 -0
  36. data/app/views/blazer/queries/run.html.erb +59 -44
  37. data/app/views/blazer/queries/show.html.erb +20 -16
  38. data/config/routes.rb +5 -0
  39. data/lib/blazer.rb +46 -2
  40. data/lib/blazer/data_source.rb +70 -0
  41. data/lib/blazer/engine.rb +6 -2
  42. data/lib/blazer/tasks.rb +12 -0
  43. data/lib/blazer/version.rb +1 -1
  44. data/lib/generators/blazer/templates/config.yml +26 -6
  45. data/lib/generators/blazer/templates/install.rb +21 -0
  46. metadata +27 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 58310083acc765c7f14b7c361d6dd5c845aa37f4
4
- data.tar.gz: 32cee84631c9ac472909833eae6eb8ad4a911e50
3
+ metadata.gz: 4f6da26ecc0d5ab8855037e401139fe007d60688
4
+ data.tar.gz: 9c1346b367f96b89240ae6c396dae6d9fd0f4f60
5
5
  SHA512:
6
- metadata.gz: 568ecb2c3426f3656221de13be4d00584de68148767b1dd02f2b7534aa1400110a2e0ecdbbd5df0ba85c4f761c21febd8bd12fe58fb69f3484c052afc323b393
7
- data.tar.gz: 65d08be78a40077860e6ecdafc9182c4cf35eb61aebf22817932669fccc012a3df502321f569d4f049f1f1827ad158c93dae4c184a34b93ec23e6246ed529a31
6
+ metadata.gz: 6d1d3782d208f2c8b00bc5bedb1d4ecbaee6e7d23cb4d90f8a76348f061d4601c9d5d8c0061dc6cc79944d78897177eb95a026b1d5939aa32575d2b2a5bf0ddc
7
+ data.tar.gz: d51440f5c1890e0eed340ec6d2d5dc974286c0f1af1781a8294e2726525a9a3b54a0e5d7f03f160ce976d01795007e7adf91a59422cab4844ddeecbaf24a6acd
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 1.0.0
2
+
3
+ - Added support for multiple data sources
4
+ - Added dashboards
5
+ - Added checks
6
+ - Added support for Redshift
7
+
1
8
  ## 0.0.8
2
9
 
3
10
  - Easier to edit queries with variables
data/README.md CHANGED
@@ -1,26 +1,30 @@
1
1
  # Blazer
2
2
 
3
- Share data effortlessly with your team
3
+ Explore your data with SQL. Easily create charts and dashboards, and share them with your team.
4
4
 
5
- Blazer eliminates the need for many admin pages
5
+ [Try it out](http://blazer.hero2app.com)
6
6
 
7
- [Play around with the demo](https://blazerme.herokuapp.com) - data from [MovieLens](http://grouplens.org/datasets/movielens/)
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
- - **Secure** - works with your authentication system
15
+ - **Multiple data sources** - works with PostgreSQL, MySQL, and Redshift
18
16
  - **Variables** - run the same queries with different values
19
- - **Linked Columns** - link to other pages in your apps or around the web
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/database_name"
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
- It is **highly, highly recommended** to use a read only user. Keep reading to see how to create one.
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
- It is **highly, highly recommended** to protect sensitive information with views. Documentation coming soon.
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
- ## Customization
128
+ ## Queries
105
129
 
106
- Change time zone
130
+ ### Variables
107
131
 
108
- ```ruby
109
- Blazer.time_zone = "Pacific Time (US & Canada)"
132
+ Create queries with variables.
133
+
134
+ ```sql
135
+ SELECT * FROM users WHERE gender = {gender}
110
136
  ```
111
137
 
112
- Change timeout *PostgreSQL only*
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
- ```ruby
115
- Blazer.timeout = 10 # defaults to 15
140
+ ```sql
141
+ SELECT * FROM ratings WHERE rated_at >= {start_time} AND rated_at <= {end_time}
116
142
  ```
117
143
 
118
- Turn off audits
144
+ ### Smart Variables
119
145
 
120
- ```ruby
121
- Blazer.audit = false
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
- Custom user class
154
+ Instead of remembering each city’s id, users can select cities by name.
125
155
 
126
- ```ruby
127
- Blazer.user_class = "Admin"
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
- Customize user name
163
+ The first column is the value of the variable, and the second column is the label.
131
164
 
132
- ```ruby
133
- Blazer.user_name = :first_name
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
- If there are at least 2 columns and the first is a timestamp and all other columns are numeric, a line chart will be generated
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
- If there are 2 columns and the first column is a string and the second column is a numeric, a pie chart will be generated
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
- - maps
157
- - favorites
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
+ });