Subversion Repositories Integrator Subversion

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1 espaco 1
/*!
2
 * Nestable jQuery Plugin - Copyright (c) 2012 David Bushell - http://dbushell.com/
3
 * Dual-licensed under the BSD or MIT licenses
4
 */
5
;(function($, window, document, undefined)
6
{
7
    var hasTouch = 'ontouchstart' in window;
8
 
9
    /**
10
     * Detect CSS pointer-events property
11
     * events are normally disabled on the dragging element to avoid conflicts
12
     * https://github.com/ausi/Feature-detection-technique-for-pointer-events/blob/master/modernizr-pointerevents.js
13
     */
14
    var hasPointerEvents = (function()
15
    {
16
        var el    = document.createElement('div'),
17
            docEl = document.documentElement;
18
        if (!('pointerEvents' in el.style)) {
19
            return false;
20
        }
21
        el.style.pointerEvents = 'auto';
22
        el.style.pointerEvents = 'x';
23
        docEl.appendChild(el);
24
        var supports = window.getComputedStyle && window.getComputedStyle(el, '').pointerEvents === 'auto';
25
        docEl.removeChild(el);
26
        return !!supports;
27
    })();
28
 
29
    var eStart  = hasTouch ? 'touchstart'  : 'mousedown',
30
        eMove   = hasTouch ? 'touchmove'   : 'mousemove',
31
        eEnd    = hasTouch ? 'touchend'    : 'mouseup';
32
        eCancel = hasTouch ? 'touchcancel' : 'mouseup';
33
 
34
    var defaults = {
35
            listNodeName    : 'ol',
36
            itemNodeName    : 'li',
37
            rootClass       : 'dd',
38
            listClass       : 'dd-list',
39
            itemClass       : 'dd-item',
40
            dragClass       : 'dd-dragel',
41
            handleClass     : 'dd-handle',
42
            collapsedClass  : 'dd-collapsed',
43
            placeClass      : 'dd-placeholder',
44
            noDragClass     : 'dd-nodrag',
45
            emptyClass      : 'dd-empty',
46
            expandBtnHTML   : '<button data-action="expand" type="button">Expand</button>',
47
            collapseBtnHTML : '<button data-action="collapse" type="button">Collapse</button>',
48
            group           : 0,
49
            maxDepth        : 5,
50
            threshold       : 20,
51
 
52
            //method for call when an item has been successfully dropped
53
            //method has 1 argument in which sends an object containing all
54
            //necessary details
55
            dropCallback    : null
56
        };
57
 
58
    function Plugin(element, options)
59
    {
60
        this.w  = $(window);
61
        this.el = $(element);
62
        this.options = $.extend({}, defaults, options);
63
        this.init();
64
    }
65
 
66
    Plugin.prototype = {
67
 
68
        init: function()
69
        {
70
            var list = this;
71
 
72
            list.reset();
73
 
74
            list.el.data('nestable-group', this.options.group);
75
 
76
            list.placeEl = $('<div class="' + list.options.placeClass + '"/>');
77
 
78
            $.each(this.el.find(list.options.itemNodeName), function(k, el) {
79
                list.setParent($(el));
80
            });
81
 
82
            list.el.on('click', 'button', function(e) {
83
                if (list.dragEl || (!hasTouch && e.button !== 0)) {
84
                    return;
85
                }
86
                var target = $(e.currentTarget),
87
                    action = target.data('action'),
88
                    item   = target.parent(list.options.itemNodeName);
89
                if (action === 'collapse') {
90
                    list.collapseItem(item);
91
                }
92
                if (action === 'expand') {
93
                    list.expandItem(item);
94
                }
95
            });
96
 
97
            var onStartEvent = function(e)
98
            {
99
                var handle = $(e.target);
100
                if (!handle.hasClass(list.options.handleClass)) {
101
                    if (handle.closest('.' + list.options.noDragClass).length) {
102
                        return;
103
                    }
104
                    handle = handle.closest('.' + list.options.handleClass);
105
                }
106
                if (!handle.length || list.dragEl || (!hasTouch && e.button !== 0) || (hasTouch && e.touches.length !== 1)) {
107
                    return;
108
                }
109
                e.preventDefault();
110
                list.dragStart(hasTouch ? e.touches[0] : e);
111
            };
112
 
113
            var onMoveEvent = function(e)
114
            {
115
                if (list.dragEl) {
116
                    e.preventDefault();
117
                    list.dragMove(hasTouch ? e.touches[0] : e);
118
                }
119
            };
120
 
121
            var onEndEvent = function(e)
122
            {
123
                if (list.dragEl) {
124
                    e.preventDefault();
125
                    list.dragStop(hasTouch ? e.touches[0] : e);
126
                }
127
            };
128
 
129
            if (hasTouch) {
130
                list.el[0].addEventListener(eStart, onStartEvent, false);
131
                window.addEventListener(eMove, onMoveEvent, false);
132
                window.addEventListener(eEnd, onEndEvent, false);
133
                window.addEventListener(eCancel, onEndEvent, false);
134
            } else {
135
                list.el.on(eStart, onStartEvent);
136
                list.w.on(eMove, onMoveEvent);
137
                list.w.on(eEnd, onEndEvent);
138
            }
139
 
140
        },
141
 
142
        serialize: function()
143
        {
144
            var data,
145
                depth = 0,
146
                list  = this;
147
                step  = function(level, depth)
148
                {
149
                    var array = [ ],
150
                        items = level.children(list.options.itemNodeName);
151
                    items.each(function()
152
                    {
153
                        var li   = $(this),
154
                            item = $.extend({}, li.data()),
155
                            sub  = li.children(list.options.listNodeName);
156
                        if (sub.length) {
157
                            item.children = step(sub, depth + 1);
158
                        }
159
                        array.push(item);
160
                    });
161
                    return array;
162
                };
163
            data = step(list.el.find(list.options.listNodeName).first(), depth);
164
            return data;
165
        },
166
 
167
        serialise: function()
168
        {
169
            return this.serialize();
170
        },
171
 
172
        reset: function()
173
        {
174
            this.mouse = {
175
                offsetX   : 0,
176
                offsetY   : 0,
177
                startX    : 0,
178
                startY    : 0,
179
                lastX     : 0,
180
                lastY     : 0,
181
                nowX      : 0,
182
                nowY      : 0,
183
                distX     : 0,
184
                distY     : 0,
185
                dirAx     : 0,
186
                dirX      : 0,
187
                dirY      : 0,
188
                lastDirX  : 0,
189
                lastDirY  : 0,
190
                distAxX   : 0,
191
                distAxY   : 0
192
            };
193
            this.moving     = false;
194
            this.dragEl     = null;
195
            this.dragRootEl = null;
196
            this.dragDepth  = 0;
197
            this.hasNewRoot = false;
198
            this.pointEl    = null;
199
            this.sourceRoot = null;
200
        },
201
 
202
        expandItem: function(li)
203
        {
204
            li.removeClass(this.options.collapsedClass);
205
            li.children('[data-action="expand"]').hide();
206
            li.children('[data-action="collapse"]').show();
207
            li.children(this.options.listNodeName).show();
208
        },
209
 
210
        collapseItem: function(li)
211
        {
212
            var lists = li.children(this.options.listNodeName);
213
            if (lists.length) {
214
                li.addClass(this.options.collapsedClass);
215
                li.children('[data-action="collapse"]').hide();
216
                li.children('[data-action="expand"]').show();
217
                li.children(this.options.listNodeName).hide();
218
            }
219
        },
220
 
221
        expandAll: function()
222
        {
223
            var list = this;
224
            list.el.find(list.options.itemNodeName).each(function() {
225
                list.expandItem($(this));
226
            });
227
        },
228
 
229
        collapseAll: function()
230
        {
231
            var list = this;
232
            list.el.find(list.options.itemNodeName).each(function() {
233
                list.collapseItem($(this));
234
            });
235
        },
236
 
237
        setParent: function(li)
238
        {
239
            if (li.children(this.options.listNodeName).length) {
240
                li.prepend($(this.options.expandBtnHTML));
241
                li.prepend($(this.options.collapseBtnHTML));
242
            }
243
            li.children('[data-action="expand"]').hide();
244
        },
245
 
246
        unsetParent: function(li)
247
        {
248
            li.removeClass(this.options.collapsedClass);
249
            li.children('[data-action]').remove();
250
            li.children(this.options.listNodeName).remove();
251
        },
252
 
253
        dragStart: function(e)
254
        {
255
            var mouse    = this.mouse,
256
                target   = $(e.target),
257
                dragItem = target.closest(this.options.itemNodeName);
258
 
259
            this.sourceRoot = target.closest('.' + this.options.rootClass);
260
            this.placeEl.css('height', dragItem.height());
261
 
262
            mouse.offsetX = e.offsetX !== undefined ? e.offsetX : e.pageX - target.offset().left;
263
            mouse.offsetY = e.offsetY !== undefined ? e.offsetY : e.pageY - target.offset().top;
264
            mouse.startX = mouse.lastX = e.pageX;
265
            mouse.startY = mouse.lastY = e.pageY;
266
 
267
            this.dragRootEl = this.el;
268
 
269
            this.dragEl = $(document.createElement(this.options.listNodeName)).addClass(this.options.listClass + ' ' + this.options.dragClass);
270
            this.dragEl.css('width', dragItem.width());
271
 
272
            // fix for zepto.js
273
            //dragItem.after(this.placeEl).detach().appendTo(this.dragEl);
274
            dragItem.after(this.placeEl);
275
            dragItem[0].parentNode.removeChild(dragItem[0]);
276
            dragItem.appendTo(this.dragEl);
277
 
278
            $(document.body).append(this.dragEl);
279
            this.dragEl.css({
280
                'left' : e.pageX - mouse.offsetX,
281
                'top'  : e.pageY - mouse.offsetY
282
            });
283
            // total depth of dragging item
284
            var i, depth,
285
                items = this.dragEl.find(this.options.itemNodeName);
286
            for (i = 0; i < items.length; i++) {
287
                depth = $(items[i]).parents(this.options.listNodeName).length;
288
                if (depth > this.dragDepth) {
289
                    this.dragDepth = depth;
290
                }
291
            }
292
        },
293
 
294
        dragStop: function(e)
295
        {
296
            // fix for zepto.js
297
            //this.placeEl.replaceWith(this.dragEl.children(this.options.itemNodeName + ':first').detach());
298
            var el = this.dragEl.children(this.options.itemNodeName).first();
299
            el[0].parentNode.removeChild(el[0]);
300
            this.placeEl.replaceWith(el);
301
 
302
            this.dragEl.remove();
303
            this.el.trigger('change');
304
 
305
            //Let's find out new parent id
306
            var parentItem = el.parent().parent();
307
            var parentId = null;
308
            if(parentItem !== null && !parentItem.is('.' + this.options.rootClass))
309
                parentId = parentItem.data('id');
310
 
311
            if($.isFunction(this.options.dropCallback)) {
312
              var details = {
313
                sourceId   : el.data('id'),
314
                destId     : parentId,
315
                sourceEl   : el,
316
                destParent : parentItem,
317
                destRoot   : el.closest('.' + this.options.rootClass),
318
                sourceRoot : this.sourceRoot
319
              };
320
              this.options.dropCallback.call(this, details);
321
            }
322
 
323
            if (this.hasNewRoot) {
324
                this.dragRootEl.trigger('change');
325
            }
326
            this.reset();
327
        },
328
 
329
        dragMove: function(e)
330
        {
331
            var list, parent, prev, next, depth,
332
                opt   = this.options,
333
                mouse = this.mouse;
334
 
335
            this.dragEl.css({
336
                'left' : e.pageX - mouse.offsetX,
337
                'top'  : e.pageY - mouse.offsetY
338
            });
339
 
340
            // mouse position last events
341
            mouse.lastX = mouse.nowX;
342
            mouse.lastY = mouse.nowY;
343
            // mouse position this events
344
            mouse.nowX  = e.pageX;
345
            mouse.nowY  = e.pageY;
346
            // distance mouse moved between events
347
            mouse.distX = mouse.nowX - mouse.lastX;
348
            mouse.distY = mouse.nowY - mouse.lastY;
349
            // direction mouse was moving
350
            mouse.lastDirX = mouse.dirX;
351
            mouse.lastDirY = mouse.dirY;
352
            // direction mouse is now moving (on both axis)
353
            mouse.dirX = mouse.distX === 0 ? 0 : mouse.distX > 0 ? 1 : -1;
354
            mouse.dirY = mouse.distY === 0 ? 0 : mouse.distY > 0 ? 1 : -1;
355
            // axis mouse is now moving on
356
            var newAx   = Math.abs(mouse.distX) > Math.abs(mouse.distY) ? 1 : 0;
357
 
358
            // do nothing on first move
359
            if (!mouse.moving) {
360
                mouse.dirAx  = newAx;
361
                mouse.moving = true;
362
                return;
363
            }
364
 
365
            // calc distance moved on this axis (and direction)
366
            if (mouse.dirAx !== newAx) {
367
                mouse.distAxX = 0;
368
                mouse.distAxY = 0;
369
            } else {
370
                mouse.distAxX += Math.abs(mouse.distX);
371
                if (mouse.dirX !== 0 && mouse.dirX !== mouse.lastDirX) {
372
                    mouse.distAxX = 0;
373
                }
374
                mouse.distAxY += Math.abs(mouse.distY);
375
                if (mouse.dirY !== 0 && mouse.dirY !== mouse.lastDirY) {
376
                    mouse.distAxY = 0;
377
                }
378
            }
379
            mouse.dirAx = newAx;
380
 
381
            /**
382
             * move horizontal
383
             */
384
            if (mouse.dirAx && mouse.distAxX >= opt.threshold) {
385
                // reset move distance on x-axis for new phase
386
                mouse.distAxX = 0;
387
                prev = this.placeEl.prev(opt.itemNodeName);
388
                // increase horizontal level if previous sibling exists and is not collapsed
389
                if (mouse.distX > 0 && prev.length && !prev.hasClass(opt.collapsedClass)) {
390
                    // cannot increase level when item above is collapsed
391
                    list = prev.find(opt.listNodeName).last();
392
                    // check if depth limit has reached
393
                    depth = this.placeEl.parents(opt.listNodeName).length;
394
                    if (depth + this.dragDepth <= opt.maxDepth) {
395
                        // create new sub-level if one doesn't exist
396
                        if (!list.length) {
397
                            list = $('<' + opt.listNodeName + '/>').addClass(opt.listClass);
398
                            list.append(this.placeEl);
399
                            prev.append(list);
400
                            this.setParent(prev);
401
                        } else {
402
                            // else append to next level up
403
                            list = prev.children(opt.listNodeName).last();
404
                            list.append(this.placeEl);
405
                        }
406
                    }
407
                }
408
                // decrease horizontal level
409
                if (mouse.distX < 0) {
410
                    // we can't decrease a level if an item preceeds the current one
411
                    next = this.placeEl.next(opt.itemNodeName);
412
                    if (!next.length) {
413
                        parent = this.placeEl.parent();
414
                        this.placeEl.closest(opt.itemNodeName).after(this.placeEl);
415
                        if (!parent.children().length) {
416
                            this.unsetParent(parent.parent());
417
                        }
418
                    }
419
                }
420
            }
421
 
422
            var isEmpty = false;
423
 
424
            // find list item under cursor
425
            if (!hasPointerEvents) {
426
                this.dragEl[0].style.visibility = 'hidden';
427
            }
428
            this.pointEl = $(document.elementFromPoint(e.pageX - document.body.scrollLeft, e.pageY - (window.pageYOffset || document.documentElement.scrollTop)));
429
            if (!hasPointerEvents) {
430
                this.dragEl[0].style.visibility = 'visible';
431
            }
432
            if (this.pointEl.hasClass(opt.handleClass)) {
433
                this.pointEl = this.pointEl.parent(opt.itemNodeName);
434
            }
435
            if (this.pointEl.hasClass(opt.emptyClass)) {
436
                isEmpty = true;
437
            }
438
            else if (!this.pointEl.length || !this.pointEl.hasClass(opt.itemClass)) {
439
                return;
440
            }
441
 
442
            // find parent list of item under cursor
443
            var pointElRoot = this.pointEl.closest('.' + opt.rootClass),
444
                isNewRoot   = this.dragRootEl.data('nestable-id') !== pointElRoot.data('nestable-id');
445
 
446
            /**
447
             * move vertical
448
             */
449
            if (!mouse.dirAx || isNewRoot || isEmpty) {
450
                // check if groups match if dragging over new root
451
                if (isNewRoot && opt.group !== pointElRoot.data('nestable-group')) {
452
                    return;
453
                }
454
                // check depth limit
455
                depth = this.dragDepth - 1 + this.pointEl.parents(opt.listNodeName).length;
456
                if (depth > opt.maxDepth) {
457
                    return;
458
                }
459
                var before = e.pageY < (this.pointEl.offset().top + this.pointEl.height() / 2);
460
                    parent = this.placeEl.parent();
461
                // if empty create new list to replace empty placeholder
462
                if (isEmpty) {
463
                    list = $(document.createElement(opt.listNodeName)).addClass(opt.listClass);
464
                    list.append(this.placeEl);
465
                    this.pointEl.replaceWith(list);
466
                }
467
                else if (before) {
468
                    this.pointEl.before(this.placeEl);
469
                }
470
                else {
471
                    this.pointEl.after(this.placeEl);
472
                }
473
                if (!parent.children().length) {
474
                    this.unsetParent(parent.parent());
475
                }
476
                if (!this.dragRootEl.find(opt.itemNodeName).length) {
477
                    this.dragRootEl.append('<div class="' + opt.emptyClass + '"/>');
478
                }
479
                // parent root list has changed
480
                if (isNewRoot) {
481
                    this.dragRootEl = pointElRoot;
482
                    this.hasNewRoot = this.el[0] !== this.dragRootEl[0];
483
                }
484
            }
485
        }
486
 
487
    };
488
 
489
    $.fn.nestable = function(params)
490
    {
491
        var lists  = this,
492
            retval = this;
493
 
494
        lists.each(function()
495
        {
496
            var plugin = $(this).data("nestable");
497
 
498
            if (!plugin) {
499
                $(this).data("nestable", new Plugin(this, params));
500
                $(this).data("nestable-id", new Date().getTime());
501
            } else {
502
                if (typeof params === 'string' && typeof plugin[params] === 'function') {
503
                    retval = plugin[params]();
504
                }
505
            }
506
        });
507
 
508
        return retval || lists;
509
    };
510
 
511
})(window.jQuery || window.Zepto, window, document);