Project

General

Profile

Download (18.3 KB) Statistics
| Branch: | Tag: | Revision:
1
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2
// 
3
// See scriptaculous.js for full license.
4

    
5
/*--------------------------------------------------------------------------*/
6

    
7
var Droppables = {
8
  drops: [],
9

    
10
  remove: function(element) {
11
    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
12
  },
13

    
14
  add: function(element) {
15
    element = $(element);
16
    var options = Object.extend({
17
      greedy:     true,
18
      hoverclass: null  
19
    }, arguments[1] || {});
20

    
21
    // cache containers
22
    if(options.containment) {
23
      options._containers = [];
24
      var containment = options.containment;
25
      if((typeof containment == 'object') && 
26
        (containment.constructor == Array)) {
27
        containment.each( function(c) { options._containers.push($(c)) });
28
      } else {
29
        options._containers.push($(containment));
30
      }
31
    }
32
    
33
    if(options.accept) options.accept = [options.accept].flatten();
34

    
35
    Element.makePositioned(element); // fix IE
36
    options.element = element;
37

    
38
    this.drops.push(options);
39
  },
40

    
41
  isContained: function(element, drop) {
42
    var parentNode = element.parentNode;
43
    return drop._containers.detect(function(c) { return parentNode == c });
44
  },
45

    
46
  isAffected: function(point, element, drop) {
47
    return (
48
      (drop.element!=element) &&
49
      ((!drop._containers) ||
50
        this.isContained(element, drop)) &&
51
      ((!drop.accept) ||
52
        (Element.classNames(element).detect( 
53
          function(v) { return drop.accept.include(v) } ) )) &&
54
      Position.within(drop.element, point[0], point[1]) );
55
  },
56

    
57
  deactivate: function(drop) {
58
    if(drop.hoverclass)
59
      Element.removeClassName(drop.element, drop.hoverclass);
60
    this.last_active = null;
61
  },
62

    
63
  activate: function(drop) {
64
    if(drop.hoverclass)
65
      Element.addClassName(drop.element, drop.hoverclass);
66
    this.last_active = drop;
67
  },
68

    
69
  show: function(point, element) {
70
    if(!this.drops.length) return;
71
    
72
    if(this.last_active) this.deactivate(this.last_active);
73
    this.drops.each( function(drop) {
74
      if(Droppables.isAffected(point, element, drop)) {
75
        if(drop.onHover)
76
           drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
77
        if(drop.greedy) { 
78
          Droppables.activate(drop);
79
          throw $break;
80
        }
81
      }
82
    });
83
  },
84

    
85
  fire: function(event, element) {
86
    if(!this.last_active) return;
87
    Position.prepare();
88

    
89
    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
90
      if (this.last_active.onDrop) 
91
        this.last_active.onDrop(element, this.last_active.element, event);
92
  },
93

    
94
  reset: function() {
95
    if(this.last_active)
96
      this.deactivate(this.last_active);
97
  }
98
}
99

    
100
var Draggables = {
101
  drags: [],
102
  observers: [],
103
  
104
  register: function(draggable) {
105
    if(this.drags.length == 0) {
106
      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
107
      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
108
      this.eventKeypress  = this.keyPress.bindAsEventListener(this);
109
      
110
      Event.observe(document, "mouseup", this.eventMouseUp);
111
      Event.observe(document, "mousemove", this.eventMouseMove);
112
      Event.observe(document, "keypress", this.eventKeypress);
113
    }
114
    this.drags.push(draggable);
115
  },
116
  
117
  unregister: function(draggable) {
118
    this.drags = this.drags.reject(function(d) { return d==draggable });
119
    if(this.drags.length == 0) {
120
      Event.stopObserving(document, "mouseup", this.eventMouseUp);
121
      Event.stopObserving(document, "mousemove", this.eventMouseMove);
122
      Event.stopObserving(document, "keypress", this.eventKeypress);
123
    }
124
  },
125
  
126
  activate: function(draggable) {
127
    window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
128
    this.activeDraggable = draggable;
129
  },
130
  
131
  deactivate: function(draggbale) {
132
    this.activeDraggable = null;
133
  },
134
  
135
  updateDrag: function(event) {
136
    if(!this.activeDraggable) return;
137
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
138
    // Mozilla-based browsers fire successive mousemove events with
139
    // the same coordinates, prevent needless redrawing (moz bug?)
140
    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
141
    this._lastPointer = pointer;
142
    this.activeDraggable.updateDrag(event, pointer);
143
  },
144
  
145
  endDrag: function(event) {
146
    if(!this.activeDraggable) return;
147
    this._lastPointer = null;
148
    this.activeDraggable.endDrag(event);
149
    this.activeDraggable = null;
150
  },
151
  
152
  keyPress: function(event) {
153
    if(this.activeDraggable)
154
      this.activeDraggable.keyPress(event);
155
  },
156
  
157
  addObserver: function(observer) {
158
    this.observers.push(observer);
159
    this._cacheObserverCallbacks();
160
  },
161
  
162
  removeObserver: function(element) {  // element instead of observer fixes mem leaks
163
    this.observers = this.observers.reject( function(o) { return o.element==element });
164
    this._cacheObserverCallbacks();
165
  },
166
  
167
  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
168
    if(this[eventName+'Count'] > 0)
169
      this.observers.each( function(o) {
170
        if(o[eventName]) o[eventName](eventName, draggable, event);
171
      });
172
  },
173
  
174
  _cacheObserverCallbacks: function() {
175
    ['onStart','onEnd','onDrag'].each( function(eventName) {
176
      Draggables[eventName+'Count'] = Draggables.observers.select(
177
        function(o) { return o[eventName]; }
178
      ).length;
179
    });
180
  }
181
}
182

    
183
/*--------------------------------------------------------------------------*/
184

    
185
var Draggable = Class.create();
186
Draggable.prototype = {
187
  initialize: function(element) {
188
    var options = Object.extend({
189
      handle: false,
190
      starteffect: function(element) { 
191
        new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); 
192
      },
193
      reverteffect: function(element, top_offset, left_offset) {
194
        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
195
        element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur});
196
      },
197
      endeffect: function(element) { 
198
        new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
199
      },
200
      zindex: 1000,
201
      revert: false,
202
      snap: false   // false, or xy or [x,y] or function(x,y){ return [x,y] }
203
    }, arguments[1] || {});
204

    
205
    this.element = $(element);
206
    
207
    if(options.handle && (typeof options.handle == 'string'))
208
      this.handle = Element.childrenWithClassName(this.element, options.handle)[0];  
209
    if(!this.handle) this.handle = $(options.handle);
210
    if(!this.handle) this.handle = this.element;
211

    
212
    Element.makePositioned(this.element); // fix IE    
213

    
214
    this.delta    = this.currentDelta();
215
    this.options  = options;
216
    this.dragging = false;   
217

    
218
    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
219
    Event.observe(this.handle, "mousedown", this.eventMouseDown);
220
    
221
    Draggables.register(this);
222
  },
223
  
224
  destroy: function() {
225
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
226
    Draggables.unregister(this);
227
  },
228
  
229
  currentDelta: function() {
230
    return([
231
      parseInt(Element.getStyle(this.element,'left') || '0'),
232
      parseInt(Element.getStyle(this.element,'top') || '0')]);
233
  },
234
  
235
  initDrag: function(event) {
236
    if(Event.isLeftClick(event)) {    
237
      // abort on form elements, fixes a Firefox issue
238
      var src = Event.element(event);
239
      if(src.tagName && (
240
        src.tagName=='INPUT' ||
241
        src.tagName=='SELECT' ||
242
        src.tagName=='BUTTON' ||
243
        src.tagName=='TEXTAREA')) return;
244
        
245
      if(this.element._revert) {
246
        this.element._revert.cancel();
247
        this.element._revert = null;
248
      }
249
      
250
      var pointer = [Event.pointerX(event), Event.pointerY(event)];
251
      var pos     = Position.cumulativeOffset(this.element);
252
      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
253
      
254
      Draggables.activate(this);
255
      Event.stop(event);
256
    }
257
  },
258
  
259
  startDrag: function(event) {
260
    this.dragging = true;
261
    
262
    if(this.options.zindex) {
263
      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
264
      this.element.style.zIndex = this.options.zindex;
265
    }
266
    
267
    if(this.options.ghosting) {
268
      this._clone = this.element.cloneNode(true);
269
      Position.absolutize(this.element);
270
      this.element.parentNode.insertBefore(this._clone, this.element);
271
    }
272
    
273
    Draggables.notify('onStart', this, event);
274
    if(this.options.starteffect) this.options.starteffect(this.element);
275
  },
276
  
277
  updateDrag: function(event, pointer) {
278
    if(!this.dragging) this.startDrag(event);
279
    Position.prepare();
280
    Droppables.show(pointer, this.element);
281
    Draggables.notify('onDrag', this, event);
282
    this.draw(pointer);
283
    if(this.options.change) this.options.change(this);
284
    
285
    // fix AppleWebKit rendering
286
    if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
287
    Event.stop(event);
288
  },
289
  
290
  finishDrag: function(event, success) {
291
    this.dragging = false;
292

    
293
    if(this.options.ghosting) {
294
      Position.relativize(this.element);
295
      Element.remove(this._clone);
296
      this._clone = null;
297
    }
298

    
299
    if(success) Droppables.fire(event, this.element);
300
    Draggables.notify('onEnd', this, event);
301

    
302
    var revert = this.options.revert;
303
    if(revert && typeof revert == 'function') revert = revert(this.element);
304
    
305
    var d = this.currentDelta();
306
    if(revert && this.options.reverteffect) {
307
      this.options.reverteffect(this.element, 
308
        d[1]-this.delta[1], d[0]-this.delta[0]);
309
    } else {
310
      this.delta = d;
311
    }
312

    
313
    if(this.options.zindex)
314
      this.element.style.zIndex = this.originalZ;
315

    
316
    if(this.options.endeffect) 
317
      this.options.endeffect(this.element);
318

    
319
    Draggables.deactivate(this);
320
    Droppables.reset();
321
  },
322
  
323
  keyPress: function(event) {
324
    if(!event.keyCode==Event.KEY_ESC) return;
325
    this.finishDrag(event, false);
326
    Event.stop(event);
327
  },
328
  
329
  endDrag: function(event) {
330
    if(!this.dragging) return;
331
    this.finishDrag(event, true);
332
    Event.stop(event);
333
  },
334
  
335
  draw: function(point) {
336
    var pos = Position.cumulativeOffset(this.element);
337
    var d = this.currentDelta();
338
    pos[0] -= d[0]; pos[1] -= d[1];
339
    
340
    var p = [0,1].map(function(i){ return (point[i]-pos[i]-this.offset[i]) }.bind(this));
341
    
342
    if(this.options.snap) {
343
      if(typeof this.options.snap == 'function') {
344
        p = this.options.snap(p[0],p[1]);
345
      } else {
346
      if(this.options.snap instanceof Array) {
347
        p = p.map( function(v, i) {
348
          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
349
      } else {
350
        p = p.map( function(v) {
351
          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
352
      }
353
    }}
354
    
355
    var style = this.element.style;
356
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
357
      style.left = p[0] + "px";
358
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
359
      style.top  = p[1] + "px";
360
    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
361
  }
362
}
363

    
364
/*--------------------------------------------------------------------------*/
365

    
366
var SortableObserver = Class.create();
367
SortableObserver.prototype = {
368
  initialize: function(element, observer) {
369
    this.element   = $(element);
370
    this.observer  = observer;
371
    this.lastValue = Sortable.serialize(this.element);
372
  },
373
  
374
  onStart: function() {
375
    this.lastValue = Sortable.serialize(this.element);
376
  },
377
  
378
  onEnd: function() {
379
    Sortable.unmark();
380
    if(this.lastValue != Sortable.serialize(this.element))
381
      this.observer(this.element)
382
  }
383
}
384

    
385
var Sortable = {
386
  sortables: new Array(),
387
  
388
  options: function(element){
389
    element = $(element);
390
    return this.sortables.detect(function(s) { return s.element == element });
391
  },
392
  
393
  destroy: function(element){
394
    element = $(element);
395
    this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
396
      Draggables.removeObserver(s.element);
397
      s.droppables.each(function(d){ Droppables.remove(d) });
398
      s.draggables.invoke('destroy');
399
    });
400
    this.sortables = this.sortables.reject(function(s) { return s.element == element });
401
  },
402
  
403
  create: function(element) {
404
    element = $(element);
405
    var options = Object.extend({ 
406
      element:     element,
407
      tag:         'li',       // assumes li children, override with tag: 'tagname'
408
      dropOnEmpty: false,
409
      tree:        false,      // fixme: unimplemented
410
      overlap:     'vertical', // one of 'vertical', 'horizontal'
411
      constraint:  'vertical', // one of 'vertical', 'horizontal', false
412
      containment: element,    // also takes array of elements (or id's); or false
413
      handle:      false,      // or a CSS class
414
      only:        false,
415
      hoverclass:  null,
416
      ghosting:    false,
417
      format:      null,
418
      onChange:    Prototype.emptyFunction,
419
      onUpdate:    Prototype.emptyFunction
420
    }, arguments[1] || {});
421

    
422
    // clear any old sortable with same element
423
    this.destroy(element);
424

    
425
    // build options for the draggables
426
    var options_for_draggable = {
427
      revert:      true,
428
      ghosting:    options.ghosting,
429
      constraint:  options.constraint,
430
      handle:      options.handle };
431

    
432
    if(options.starteffect)
433
      options_for_draggable.starteffect = options.starteffect;
434

    
435
    if(options.reverteffect)
436
      options_for_draggable.reverteffect = options.reverteffect;
437
    else
438
      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
439
        element.style.top  = 0;
440
        element.style.left = 0;
441
      };
442

    
443
    if(options.endeffect)
444
      options_for_draggable.endeffect = options.endeffect;
445

    
446
    if(options.zindex)
447
      options_for_draggable.zindex = options.zindex;
448

    
449
    // build options for the droppables  
450
    var options_for_droppable = {
451
      overlap:     options.overlap,
452
      containment: options.containment,
453
      hoverclass:  options.hoverclass,
454
      onHover:     Sortable.onHover,
455
      greedy:      !options.dropOnEmpty
456
    }
457

    
458
    // fix for gecko engine
459
    Element.cleanWhitespace(element); 
460

    
461
    options.draggables = [];
462
    options.droppables = [];
463

    
464
    // make it so
465

    
466
    // drop on empty handling
467
    if(options.dropOnEmpty) {
468
      Droppables.add(element,
469
        {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
470
      options.droppables.push(element);
471
    }
472

    
473
    (this.findElements(element, options) || []).each( function(e) {
474
      // handles are per-draggable
475
      var handle = options.handle ? 
476
        Element.childrenWithClassName(e, options.handle)[0] : e;    
477
      options.draggables.push(
478
        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
479
      Droppables.add(e, options_for_droppable);
480
      options.droppables.push(e);      
481
    });
482

    
483
    // keep reference
484
    this.sortables.push(options);
485

    
486
    // for onupdate
487
    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
488

    
489
  },
490

    
491
  // return all suitable-for-sortable elements in a guaranteed order
492
  findElements: function(element, options) {
493
    if(!element.hasChildNodes()) return null;
494
    var elements = [];
495
    $A(element.childNodes).each( function(e) {
496
      if(e.tagName && e.tagName.toUpperCase()==options.tag.toUpperCase() &&
497
        (!options.only || (Element.hasClassName(e, options.only))))
498
          elements.push(e);
499
      if(options.tree) {
500
        var grandchildren = this.findElements(e, options);
501
        if(grandchildren) elements.push(grandchildren);
502
      }
503
    });
504

    
505
    return (elements.length>0 ? elements.flatten() : null);
506
  },
507

    
508
  onHover: function(element, dropon, overlap) {
509
    if(overlap>0.5) {
510
      Sortable.mark(dropon, 'before');
511
      if(dropon.previousSibling != element) {
512
        var oldParentNode = element.parentNode;
513
        element.style.visibility = "hidden"; // fix gecko rendering
514
        dropon.parentNode.insertBefore(element, dropon);
515
        if(dropon.parentNode!=oldParentNode) 
516
          Sortable.options(oldParentNode).onChange(element);
517
        Sortable.options(dropon.parentNode).onChange(element);
518
      }
519
    } else {
520
      Sortable.mark(dropon, 'after');
521
      var nextElement = dropon.nextSibling || null;
522
      if(nextElement != element) {
523
        var oldParentNode = element.parentNode;
524
        element.style.visibility = "hidden"; // fix gecko rendering
525
        dropon.parentNode.insertBefore(element, nextElement);
526
        if(dropon.parentNode!=oldParentNode) 
527
          Sortable.options(oldParentNode).onChange(element);
528
        Sortable.options(dropon.parentNode).onChange(element);
529
      }
530
    }
531
  },
532

    
533
  onEmptyHover: function(element, dropon) {
534
    if(element.parentNode!=dropon) {
535
      var oldParentNode = element.parentNode;
536
      dropon.appendChild(element);
537
      Sortable.options(oldParentNode).onChange(element);
538
      Sortable.options(dropon).onChange(element);
539
    }
540
  },
541

    
542
  unmark: function() {
543
    if(Sortable._marker) Element.hide(Sortable._marker);
544
  },
545

    
546
  mark: function(dropon, position) {
547
    // mark on ghosting only
548
    var sortable = Sortable.options(dropon.parentNode);
549
    if(sortable && !sortable.ghosting) return; 
550

    
551
    if(!Sortable._marker) {
552
      Sortable._marker = $('dropmarker') || document.createElement('DIV');
553
      Element.hide(Sortable._marker);
554
      Element.addClassName(Sortable._marker, 'dropmarker');
555
      Sortable._marker.style.position = 'absolute';
556
      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
557
    }    
558
    var offsets = Position.cumulativeOffset(dropon);
559
    Sortable._marker.style.left = offsets[0] + 'px';
560
    Sortable._marker.style.top = offsets[1] + 'px';
561
    
562
    if(position=='after')
563
      if(sortable.overlap == 'horizontal') 
564
        Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
565
      else
566
        Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
567
    
568
    Element.show(Sortable._marker);
569
  },
570

    
571
  serialize: function(element) {
572
    element = $(element);
573
    var sortableOptions = this.options(element);
574
    var options = Object.extend({
575
      tag:  sortableOptions.tag,
576
      only: sortableOptions.only,
577
      name: element.id,
578
      format: sortableOptions.format || /^[^_]*_(.*)$/
579
    }, arguments[1] || {});
580
    return $(this.findElements(element, options) || []).map( function(item) {
581
      return (encodeURIComponent(options.name) + "[]=" + 
582
              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
583
    }).join("&");
584
  }
585
}
(3-3/8)