Mercurial > hg > soundsoftware-site
comparison public/javascripts/.svn/text-base/dragdrop.js.svn-base @ 0:513646585e45
* Import Redmine trunk SVN rev 3859
author | Chris Cannam |
---|---|
date | Fri, 23 Jul 2010 15:52:44 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:513646585e45 |
---|---|
1 // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) | |
2 // (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) | |
3 // | |
4 // script.aculo.us is freely distributable under the terms of an MIT-style license. | |
5 // For details, see the script.aculo.us web site: http://script.aculo.us/ | |
6 | |
7 if(Object.isUndefined(Effect)) | |
8 throw("dragdrop.js requires including script.aculo.us' effects.js library"); | |
9 | |
10 var Droppables = { | |
11 drops: [], | |
12 | |
13 remove: function(element) { | |
14 this.drops = this.drops.reject(function(d) { return d.element==$(element) }); | |
15 }, | |
16 | |
17 add: function(element) { | |
18 element = $(element); | |
19 var options = Object.extend({ | |
20 greedy: true, | |
21 hoverclass: null, | |
22 tree: false | |
23 }, arguments[1] || { }); | |
24 | |
25 // cache containers | |
26 if(options.containment) { | |
27 options._containers = []; | |
28 var containment = options.containment; | |
29 if(Object.isArray(containment)) { | |
30 containment.each( function(c) { options._containers.push($(c)) }); | |
31 } else { | |
32 options._containers.push($(containment)); | |
33 } | |
34 } | |
35 | |
36 if(options.accept) options.accept = [options.accept].flatten(); | |
37 | |
38 Element.makePositioned(element); // fix IE | |
39 options.element = element; | |
40 | |
41 this.drops.push(options); | |
42 }, | |
43 | |
44 findDeepestChild: function(drops) { | |
45 deepest = drops[0]; | |
46 | |
47 for (i = 1; i < drops.length; ++i) | |
48 if (Element.isParent(drops[i].element, deepest.element)) | |
49 deepest = drops[i]; | |
50 | |
51 return deepest; | |
52 }, | |
53 | |
54 isContained: function(element, drop) { | |
55 var containmentNode; | |
56 if(drop.tree) { | |
57 containmentNode = element.treeNode; | |
58 } else { | |
59 containmentNode = element.parentNode; | |
60 } | |
61 return drop._containers.detect(function(c) { return containmentNode == c }); | |
62 }, | |
63 | |
64 isAffected: function(point, element, drop) { | |
65 return ( | |
66 (drop.element!=element) && | |
67 ((!drop._containers) || | |
68 this.isContained(element, drop)) && | |
69 ((!drop.accept) || | |
70 (Element.classNames(element).detect( | |
71 function(v) { return drop.accept.include(v) } ) )) && | |
72 Position.within(drop.element, point[0], point[1]) ); | |
73 }, | |
74 | |
75 deactivate: function(drop) { | |
76 if(drop.hoverclass) | |
77 Element.removeClassName(drop.element, drop.hoverclass); | |
78 this.last_active = null; | |
79 }, | |
80 | |
81 activate: function(drop) { | |
82 if(drop.hoverclass) | |
83 Element.addClassName(drop.element, drop.hoverclass); | |
84 this.last_active = drop; | |
85 }, | |
86 | |
87 show: function(point, element) { | |
88 if(!this.drops.length) return; | |
89 var drop, affected = []; | |
90 | |
91 this.drops.each( function(drop) { | |
92 if(Droppables.isAffected(point, element, drop)) | |
93 affected.push(drop); | |
94 }); | |
95 | |
96 if(affected.length>0) | |
97 drop = Droppables.findDeepestChild(affected); | |
98 | |
99 if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); | |
100 if (drop) { | |
101 Position.within(drop.element, point[0], point[1]); | |
102 if(drop.onHover) | |
103 drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); | |
104 | |
105 if (drop != this.last_active) Droppables.activate(drop); | |
106 } | |
107 }, | |
108 | |
109 fire: function(event, element) { | |
110 if(!this.last_active) return; | |
111 Position.prepare(); | |
112 | |
113 if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) | |
114 if (this.last_active.onDrop) { | |
115 this.last_active.onDrop(element, this.last_active.element, event); | |
116 return true; | |
117 } | |
118 }, | |
119 | |
120 reset: function() { | |
121 if(this.last_active) | |
122 this.deactivate(this.last_active); | |
123 } | |
124 }; | |
125 | |
126 var Draggables = { | |
127 drags: [], | |
128 observers: [], | |
129 | |
130 register: function(draggable) { | |
131 if(this.drags.length == 0) { | |
132 this.eventMouseUp = this.endDrag.bindAsEventListener(this); | |
133 this.eventMouseMove = this.updateDrag.bindAsEventListener(this); | |
134 this.eventKeypress = this.keyPress.bindAsEventListener(this); | |
135 | |
136 Event.observe(document, "mouseup", this.eventMouseUp); | |
137 Event.observe(document, "mousemove", this.eventMouseMove); | |
138 Event.observe(document, "keypress", this.eventKeypress); | |
139 } | |
140 this.drags.push(draggable); | |
141 }, | |
142 | |
143 unregister: function(draggable) { | |
144 this.drags = this.drags.reject(function(d) { return d==draggable }); | |
145 if(this.drags.length == 0) { | |
146 Event.stopObserving(document, "mouseup", this.eventMouseUp); | |
147 Event.stopObserving(document, "mousemove", this.eventMouseMove); | |
148 Event.stopObserving(document, "keypress", this.eventKeypress); | |
149 } | |
150 }, | |
151 | |
152 activate: function(draggable) { | |
153 if(draggable.options.delay) { | |
154 this._timeout = setTimeout(function() { | |
155 Draggables._timeout = null; | |
156 window.focus(); | |
157 Draggables.activeDraggable = draggable; | |
158 }.bind(this), draggable.options.delay); | |
159 } else { | |
160 window.focus(); // allows keypress events if window isn't currently focused, fails for Safari | |
161 this.activeDraggable = draggable; | |
162 } | |
163 }, | |
164 | |
165 deactivate: function() { | |
166 this.activeDraggable = null; | |
167 }, | |
168 | |
169 updateDrag: function(event) { | |
170 if(!this.activeDraggable) return; | |
171 var pointer = [Event.pointerX(event), Event.pointerY(event)]; | |
172 // Mozilla-based browsers fire successive mousemove events with | |
173 // the same coordinates, prevent needless redrawing (moz bug?) | |
174 if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; | |
175 this._lastPointer = pointer; | |
176 | |
177 this.activeDraggable.updateDrag(event, pointer); | |
178 }, | |
179 | |
180 endDrag: function(event) { | |
181 if(this._timeout) { | |
182 clearTimeout(this._timeout); | |
183 this._timeout = null; | |
184 } | |
185 if(!this.activeDraggable) return; | |
186 this._lastPointer = null; | |
187 this.activeDraggable.endDrag(event); | |
188 this.activeDraggable = null; | |
189 }, | |
190 | |
191 keyPress: function(event) { | |
192 if(this.activeDraggable) | |
193 this.activeDraggable.keyPress(event); | |
194 }, | |
195 | |
196 addObserver: function(observer) { | |
197 this.observers.push(observer); | |
198 this._cacheObserverCallbacks(); | |
199 }, | |
200 | |
201 removeObserver: function(element) { // element instead of observer fixes mem leaks | |
202 this.observers = this.observers.reject( function(o) { return o.element==element }); | |
203 this._cacheObserverCallbacks(); | |
204 }, | |
205 | |
206 notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' | |
207 if(this[eventName+'Count'] > 0) | |
208 this.observers.each( function(o) { | |
209 if(o[eventName]) o[eventName](eventName, draggable, event); | |
210 }); | |
211 if(draggable.options[eventName]) draggable.options[eventName](draggable, event); | |
212 }, | |
213 | |
214 _cacheObserverCallbacks: function() { | |
215 ['onStart','onEnd','onDrag'].each( function(eventName) { | |
216 Draggables[eventName+'Count'] = Draggables.observers.select( | |
217 function(o) { return o[eventName]; } | |
218 ).length; | |
219 }); | |
220 } | |
221 }; | |
222 | |
223 /*--------------------------------------------------------------------------*/ | |
224 | |
225 var Draggable = Class.create({ | |
226 initialize: function(element) { | |
227 var defaults = { | |
228 handle: false, | |
229 reverteffect: function(element, top_offset, left_offset) { | |
230 var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; | |
231 new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, | |
232 queue: {scope:'_draggable', position:'end'} | |
233 }); | |
234 }, | |
235 endeffect: function(element) { | |
236 var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; | |
237 new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, | |
238 queue: {scope:'_draggable', position:'end'}, | |
239 afterFinish: function(){ | |
240 Draggable._dragging[element] = false | |
241 } | |
242 }); | |
243 }, | |
244 zindex: 1000, | |
245 revert: false, | |
246 quiet: false, | |
247 scroll: false, | |
248 scrollSensitivity: 20, | |
249 scrollSpeed: 15, | |
250 snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } | |
251 delay: 0 | |
252 }; | |
253 | |
254 if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) | |
255 Object.extend(defaults, { | |
256 starteffect: function(element) { | |
257 element._opacity = Element.getOpacity(element); | |
258 Draggable._dragging[element] = true; | |
259 new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); | |
260 } | |
261 }); | |
262 | |
263 var options = Object.extend(defaults, arguments[1] || { }); | |
264 | |
265 this.element = $(element); | |
266 | |
267 if(options.handle && Object.isString(options.handle)) | |
268 this.handle = this.element.down('.'+options.handle, 0); | |
269 | |
270 if(!this.handle) this.handle = $(options.handle); | |
271 if(!this.handle) this.handle = this.element; | |
272 | |
273 if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { | |
274 options.scroll = $(options.scroll); | |
275 this._isScrollChild = Element.childOf(this.element, options.scroll); | |
276 } | |
277 | |
278 Element.makePositioned(this.element); // fix IE | |
279 | |
280 this.options = options; | |
281 this.dragging = false; | |
282 | |
283 this.eventMouseDown = this.initDrag.bindAsEventListener(this); | |
284 Event.observe(this.handle, "mousedown", this.eventMouseDown); | |
285 | |
286 Draggables.register(this); | |
287 }, | |
288 | |
289 destroy: function() { | |
290 Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); | |
291 Draggables.unregister(this); | |
292 }, | |
293 | |
294 currentDelta: function() { | |
295 return([ | |
296 parseInt(Element.getStyle(this.element,'left') || '0'), | |
297 parseInt(Element.getStyle(this.element,'top') || '0')]); | |
298 }, | |
299 | |
300 initDrag: function(event) { | |
301 if(!Object.isUndefined(Draggable._dragging[this.element]) && | |
302 Draggable._dragging[this.element]) return; | |
303 if(Event.isLeftClick(event)) { | |
304 // abort on form elements, fixes a Firefox issue | |
305 var src = Event.element(event); | |
306 if((tag_name = src.tagName.toUpperCase()) && ( | |
307 tag_name=='INPUT' || | |
308 tag_name=='SELECT' || | |
309 tag_name=='OPTION' || | |
310 tag_name=='BUTTON' || | |
311 tag_name=='TEXTAREA')) return; | |
312 | |
313 var pointer = [Event.pointerX(event), Event.pointerY(event)]; | |
314 var pos = Position.cumulativeOffset(this.element); | |
315 this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); | |
316 | |
317 Draggables.activate(this); | |
318 Event.stop(event); | |
319 } | |
320 }, | |
321 | |
322 startDrag: function(event) { | |
323 this.dragging = true; | |
324 if(!this.delta) | |
325 this.delta = this.currentDelta(); | |
326 | |
327 if(this.options.zindex) { | |
328 this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); | |
329 this.element.style.zIndex = this.options.zindex; | |
330 } | |
331 | |
332 if(this.options.ghosting) { | |
333 this._clone = this.element.cloneNode(true); | |
334 this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); | |
335 if (!this._originallyAbsolute) | |
336 Position.absolutize(this.element); | |
337 this.element.parentNode.insertBefore(this._clone, this.element); | |
338 } | |
339 | |
340 if(this.options.scroll) { | |
341 if (this.options.scroll == window) { | |
342 var where = this._getWindowScroll(this.options.scroll); | |
343 this.originalScrollLeft = where.left; | |
344 this.originalScrollTop = where.top; | |
345 } else { | |
346 this.originalScrollLeft = this.options.scroll.scrollLeft; | |
347 this.originalScrollTop = this.options.scroll.scrollTop; | |
348 } | |
349 } | |
350 | |
351 Draggables.notify('onStart', this, event); | |
352 | |
353 if(this.options.starteffect) this.options.starteffect(this.element); | |
354 }, | |
355 | |
356 updateDrag: function(event, pointer) { | |
357 if(!this.dragging) this.startDrag(event); | |
358 | |
359 if(!this.options.quiet){ | |
360 Position.prepare(); | |
361 Droppables.show(pointer, this.element); | |
362 } | |
363 | |
364 Draggables.notify('onDrag', this, event); | |
365 | |
366 this.draw(pointer); | |
367 if(this.options.change) this.options.change(this); | |
368 | |
369 if(this.options.scroll) { | |
370 this.stopScrolling(); | |
371 | |
372 var p; | |
373 if (this.options.scroll == window) { | |
374 with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } | |
375 } else { | |
376 p = Position.page(this.options.scroll); | |
377 p[0] += this.options.scroll.scrollLeft + Position.deltaX; | |
378 p[1] += this.options.scroll.scrollTop + Position.deltaY; | |
379 p.push(p[0]+this.options.scroll.offsetWidth); | |
380 p.push(p[1]+this.options.scroll.offsetHeight); | |
381 } | |
382 var speed = [0,0]; | |
383 if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); | |
384 if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); | |
385 if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); | |
386 if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); | |
387 this.startScrolling(speed); | |
388 } | |
389 | |
390 // fix AppleWebKit rendering | |
391 if(Prototype.Browser.WebKit) window.scrollBy(0,0); | |
392 | |
393 Event.stop(event); | |
394 }, | |
395 | |
396 finishDrag: function(event, success) { | |
397 this.dragging = false; | |
398 | |
399 if(this.options.quiet){ | |
400 Position.prepare(); | |
401 var pointer = [Event.pointerX(event), Event.pointerY(event)]; | |
402 Droppables.show(pointer, this.element); | |
403 } | |
404 | |
405 if(this.options.ghosting) { | |
406 if (!this._originallyAbsolute) | |
407 Position.relativize(this.element); | |
408 delete this._originallyAbsolute; | |
409 Element.remove(this._clone); | |
410 this._clone = null; | |
411 } | |
412 | |
413 var dropped = false; | |
414 if(success) { | |
415 dropped = Droppables.fire(event, this.element); | |
416 if (!dropped) dropped = false; | |
417 } | |
418 if(dropped && this.options.onDropped) this.options.onDropped(this.element); | |
419 Draggables.notify('onEnd', this, event); | |
420 | |
421 var revert = this.options.revert; | |
422 if(revert && Object.isFunction(revert)) revert = revert(this.element); | |
423 | |
424 var d = this.currentDelta(); | |
425 if(revert && this.options.reverteffect) { | |
426 if (dropped == 0 || revert != 'failure') | |
427 this.options.reverteffect(this.element, | |
428 d[1]-this.delta[1], d[0]-this.delta[0]); | |
429 } else { | |
430 this.delta = d; | |
431 } | |
432 | |
433 if(this.options.zindex) | |
434 this.element.style.zIndex = this.originalZ; | |
435 | |
436 if(this.options.endeffect) | |
437 this.options.endeffect(this.element); | |
438 | |
439 Draggables.deactivate(this); | |
440 Droppables.reset(); | |
441 }, | |
442 | |
443 keyPress: function(event) { | |
444 if(event.keyCode!=Event.KEY_ESC) return; | |
445 this.finishDrag(event, false); | |
446 Event.stop(event); | |
447 }, | |
448 | |
449 endDrag: function(event) { | |
450 if(!this.dragging) return; | |
451 this.stopScrolling(); | |
452 this.finishDrag(event, true); | |
453 Event.stop(event); | |
454 }, | |
455 | |
456 draw: function(point) { | |
457 var pos = Position.cumulativeOffset(this.element); | |
458 if(this.options.ghosting) { | |
459 var r = Position.realOffset(this.element); | |
460 pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; | |
461 } | |
462 | |
463 var d = this.currentDelta(); | |
464 pos[0] -= d[0]; pos[1] -= d[1]; | |
465 | |
466 if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { | |
467 pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; | |
468 pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; | |
469 } | |
470 | |
471 var p = [0,1].map(function(i){ | |
472 return (point[i]-pos[i]-this.offset[i]) | |
473 }.bind(this)); | |
474 | |
475 if(this.options.snap) { | |
476 if(Object.isFunction(this.options.snap)) { | |
477 p = this.options.snap(p[0],p[1],this); | |
478 } else { | |
479 if(Object.isArray(this.options.snap)) { | |
480 p = p.map( function(v, i) { | |
481 return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); | |
482 } else { | |
483 p = p.map( function(v) { | |
484 return (v/this.options.snap).round()*this.options.snap }.bind(this)); | |
485 } | |
486 }} | |
487 | |
488 var style = this.element.style; | |
489 if((!this.options.constraint) || (this.options.constraint=='horizontal')) | |
490 style.left = p[0] + "px"; | |
491 if((!this.options.constraint) || (this.options.constraint=='vertical')) | |
492 style.top = p[1] + "px"; | |
493 | |
494 if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering | |
495 }, | |
496 | |
497 stopScrolling: function() { | |
498 if(this.scrollInterval) { | |
499 clearInterval(this.scrollInterval); | |
500 this.scrollInterval = null; | |
501 Draggables._lastScrollPointer = null; | |
502 } | |
503 }, | |
504 | |
505 startScrolling: function(speed) { | |
506 if(!(speed[0] || speed[1])) return; | |
507 this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; | |
508 this.lastScrolled = new Date(); | |
509 this.scrollInterval = setInterval(this.scroll.bind(this), 10); | |
510 }, | |
511 | |
512 scroll: function() { | |
513 var current = new Date(); | |
514 var delta = current - this.lastScrolled; | |
515 this.lastScrolled = current; | |
516 if(this.options.scroll == window) { | |
517 with (this._getWindowScroll(this.options.scroll)) { | |
518 if (this.scrollSpeed[0] || this.scrollSpeed[1]) { | |
519 var d = delta / 1000; | |
520 this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); | |
521 } | |
522 } | |
523 } else { | |
524 this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; | |
525 this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; | |
526 } | |
527 | |
528 Position.prepare(); | |
529 Droppables.show(Draggables._lastPointer, this.element); | |
530 Draggables.notify('onDrag', this); | |
531 if (this._isScrollChild) { | |
532 Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); | |
533 Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; | |
534 Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; | |
535 if (Draggables._lastScrollPointer[0] < 0) | |
536 Draggables._lastScrollPointer[0] = 0; | |
537 if (Draggables._lastScrollPointer[1] < 0) | |
538 Draggables._lastScrollPointer[1] = 0; | |
539 this.draw(Draggables._lastScrollPointer); | |
540 } | |
541 | |
542 if(this.options.change) this.options.change(this); | |
543 }, | |
544 | |
545 _getWindowScroll: function(w) { | |
546 var T, L, W, H; | |
547 with (w.document) { | |
548 if (w.document.documentElement && documentElement.scrollTop) { | |
549 T = documentElement.scrollTop; | |
550 L = documentElement.scrollLeft; | |
551 } else if (w.document.body) { | |
552 T = body.scrollTop; | |
553 L = body.scrollLeft; | |
554 } | |
555 if (w.innerWidth) { | |
556 W = w.innerWidth; | |
557 H = w.innerHeight; | |
558 } else if (w.document.documentElement && documentElement.clientWidth) { | |
559 W = documentElement.clientWidth; | |
560 H = documentElement.clientHeight; | |
561 } else { | |
562 W = body.offsetWidth; | |
563 H = body.offsetHeight; | |
564 } | |
565 } | |
566 return { top: T, left: L, width: W, height: H }; | |
567 } | |
568 }); | |
569 | |
570 Draggable._dragging = { }; | |
571 | |
572 /*--------------------------------------------------------------------------*/ | |
573 | |
574 var SortableObserver = Class.create({ | |
575 initialize: function(element, observer) { | |
576 this.element = $(element); | |
577 this.observer = observer; | |
578 this.lastValue = Sortable.serialize(this.element); | |
579 }, | |
580 | |
581 onStart: function() { | |
582 this.lastValue = Sortable.serialize(this.element); | |
583 }, | |
584 | |
585 onEnd: function() { | |
586 Sortable.unmark(); | |
587 if(this.lastValue != Sortable.serialize(this.element)) | |
588 this.observer(this.element) | |
589 } | |
590 }); | |
591 | |
592 var Sortable = { | |
593 SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, | |
594 | |
595 sortables: { }, | |
596 | |
597 _findRootElement: function(element) { | |
598 while (element.tagName.toUpperCase() != "BODY") { | |
599 if(element.id && Sortable.sortables[element.id]) return element; | |
600 element = element.parentNode; | |
601 } | |
602 }, | |
603 | |
604 options: function(element) { | |
605 element = Sortable._findRootElement($(element)); | |
606 if(!element) return; | |
607 return Sortable.sortables[element.id]; | |
608 }, | |
609 | |
610 destroy: function(element){ | |
611 element = $(element); | |
612 var s = Sortable.sortables[element.id]; | |
613 | |
614 if(s) { | |
615 Draggables.removeObserver(s.element); | |
616 s.droppables.each(function(d){ Droppables.remove(d) }); | |
617 s.draggables.invoke('destroy'); | |
618 | |
619 delete Sortable.sortables[s.element.id]; | |
620 } | |
621 }, | |
622 | |
623 create: function(element) { | |
624 element = $(element); | |
625 var options = Object.extend({ | |
626 element: element, | |
627 tag: 'li', // assumes li children, override with tag: 'tagname' | |
628 dropOnEmpty: false, | |
629 tree: false, | |
630 treeTag: 'ul', | |
631 overlap: 'vertical', // one of 'vertical', 'horizontal' | |
632 constraint: 'vertical', // one of 'vertical', 'horizontal', false | |
633 containment: element, // also takes array of elements (or id's); or false | |
634 handle: false, // or a CSS class | |
635 only: false, | |
636 delay: 0, | |
637 hoverclass: null, | |
638 ghosting: false, | |
639 quiet: false, | |
640 scroll: false, | |
641 scrollSensitivity: 20, | |
642 scrollSpeed: 15, | |
643 format: this.SERIALIZE_RULE, | |
644 | |
645 // these take arrays of elements or ids and can be | |
646 // used for better initialization performance | |
647 elements: false, | |
648 handles: false, | |
649 | |
650 onChange: Prototype.emptyFunction, | |
651 onUpdate: Prototype.emptyFunction | |
652 }, arguments[1] || { }); | |
653 | |
654 // clear any old sortable with same element | |
655 this.destroy(element); | |
656 | |
657 // build options for the draggables | |
658 var options_for_draggable = { | |
659 revert: true, | |
660 quiet: options.quiet, | |
661 scroll: options.scroll, | |
662 scrollSpeed: options.scrollSpeed, | |
663 scrollSensitivity: options.scrollSensitivity, | |
664 delay: options.delay, | |
665 ghosting: options.ghosting, | |
666 constraint: options.constraint, | |
667 handle: options.handle }; | |
668 | |
669 if(options.starteffect) | |
670 options_for_draggable.starteffect = options.starteffect; | |
671 | |
672 if(options.reverteffect) | |
673 options_for_draggable.reverteffect = options.reverteffect; | |
674 else | |
675 if(options.ghosting) options_for_draggable.reverteffect = function(element) { | |
676 element.style.top = 0; | |
677 element.style.left = 0; | |
678 }; | |
679 | |
680 if(options.endeffect) | |
681 options_for_draggable.endeffect = options.endeffect; | |
682 | |
683 if(options.zindex) | |
684 options_for_draggable.zindex = options.zindex; | |
685 | |
686 // build options for the droppables | |
687 var options_for_droppable = { | |
688 overlap: options.overlap, | |
689 containment: options.containment, | |
690 tree: options.tree, | |
691 hoverclass: options.hoverclass, | |
692 onHover: Sortable.onHover | |
693 }; | |
694 | |
695 var options_for_tree = { | |
696 onHover: Sortable.onEmptyHover, | |
697 overlap: options.overlap, | |
698 containment: options.containment, | |
699 hoverclass: options.hoverclass | |
700 }; | |
701 | |
702 // fix for gecko engine | |
703 Element.cleanWhitespace(element); | |
704 | |
705 options.draggables = []; | |
706 options.droppables = []; | |
707 | |
708 // drop on empty handling | |
709 if(options.dropOnEmpty || options.tree) { | |
710 Droppables.add(element, options_for_tree); | |
711 options.droppables.push(element); | |
712 } | |
713 | |
714 (options.elements || this.findElements(element, options) || []).each( function(e,i) { | |
715 var handle = options.handles ? $(options.handles[i]) : | |
716 (options.handle ? $(e).select('.' + options.handle)[0] : e); | |
717 options.draggables.push( | |
718 new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); | |
719 Droppables.add(e, options_for_droppable); | |
720 if(options.tree) e.treeNode = element; | |
721 options.droppables.push(e); | |
722 }); | |
723 | |
724 if(options.tree) { | |
725 (Sortable.findTreeElements(element, options) || []).each( function(e) { | |
726 Droppables.add(e, options_for_tree); | |
727 e.treeNode = element; | |
728 options.droppables.push(e); | |
729 }); | |
730 } | |
731 | |
732 // keep reference | |
733 this.sortables[element.id] = options; | |
734 | |
735 // for onupdate | |
736 Draggables.addObserver(new SortableObserver(element, options.onUpdate)); | |
737 | |
738 }, | |
739 | |
740 // return all suitable-for-sortable elements in a guaranteed order | |
741 findElements: function(element, options) { | |
742 return Element.findChildren( | |
743 element, options.only, options.tree ? true : false, options.tag); | |
744 }, | |
745 | |
746 findTreeElements: function(element, options) { | |
747 return Element.findChildren( | |
748 element, options.only, options.tree ? true : false, options.treeTag); | |
749 }, | |
750 | |
751 onHover: function(element, dropon, overlap) { | |
752 if(Element.isParent(dropon, element)) return; | |
753 | |
754 if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { | |
755 return; | |
756 } else if(overlap>0.5) { | |
757 Sortable.mark(dropon, 'before'); | |
758 if(dropon.previousSibling != element) { | |
759 var oldParentNode = element.parentNode; | |
760 element.style.visibility = "hidden"; // fix gecko rendering | |
761 dropon.parentNode.insertBefore(element, dropon); | |
762 if(dropon.parentNode!=oldParentNode) | |
763 Sortable.options(oldParentNode).onChange(element); | |
764 Sortable.options(dropon.parentNode).onChange(element); | |
765 } | |
766 } else { | |
767 Sortable.mark(dropon, 'after'); | |
768 var nextElement = dropon.nextSibling || null; | |
769 if(nextElement != element) { | |
770 var oldParentNode = element.parentNode; | |
771 element.style.visibility = "hidden"; // fix gecko rendering | |
772 dropon.parentNode.insertBefore(element, nextElement); | |
773 if(dropon.parentNode!=oldParentNode) | |
774 Sortable.options(oldParentNode).onChange(element); | |
775 Sortable.options(dropon.parentNode).onChange(element); | |
776 } | |
777 } | |
778 }, | |
779 | |
780 onEmptyHover: function(element, dropon, overlap) { | |
781 var oldParentNode = element.parentNode; | |
782 var droponOptions = Sortable.options(dropon); | |
783 | |
784 if(!Element.isParent(dropon, element)) { | |
785 var index; | |
786 | |
787 var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); | |
788 var child = null; | |
789 | |
790 if(children) { | |
791 var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); | |
792 | |
793 for (index = 0; index < children.length; index += 1) { | |
794 if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { | |
795 offset -= Element.offsetSize (children[index], droponOptions.overlap); | |
796 } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { | |
797 child = index + 1 < children.length ? children[index + 1] : null; | |
798 break; | |
799 } else { | |
800 child = children[index]; | |
801 break; | |
802 } | |
803 } | |
804 } | |
805 | |
806 dropon.insertBefore(element, child); | |
807 | |
808 Sortable.options(oldParentNode).onChange(element); | |
809 droponOptions.onChange(element); | |
810 } | |
811 }, | |
812 | |
813 unmark: function() { | |
814 if(Sortable._marker) Sortable._marker.hide(); | |
815 }, | |
816 | |
817 mark: function(dropon, position) { | |
818 // mark on ghosting only | |
819 var sortable = Sortable.options(dropon.parentNode); | |
820 if(sortable && !sortable.ghosting) return; | |
821 | |
822 if(!Sortable._marker) { | |
823 Sortable._marker = | |
824 ($('dropmarker') || Element.extend(document.createElement('DIV'))). | |
825 hide().addClassName('dropmarker').setStyle({position:'absolute'}); | |
826 document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); | |
827 } | |
828 var offsets = Position.cumulativeOffset(dropon); | |
829 Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); | |
830 | |
831 if(position=='after') | |
832 if(sortable.overlap == 'horizontal') | |
833 Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); | |
834 else | |
835 Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); | |
836 | |
837 Sortable._marker.show(); | |
838 }, | |
839 | |
840 _tree: function(element, options, parent) { | |
841 var children = Sortable.findElements(element, options) || []; | |
842 | |
843 for (var i = 0; i < children.length; ++i) { | |
844 var match = children[i].id.match(options.format); | |
845 | |
846 if (!match) continue; | |
847 | |
848 var child = { | |
849 id: encodeURIComponent(match ? match[1] : null), | |
850 element: element, | |
851 parent: parent, | |
852 children: [], | |
853 position: parent.children.length, | |
854 container: $(children[i]).down(options.treeTag) | |
855 }; | |
856 | |
857 /* Get the element containing the children and recurse over it */ | |
858 if (child.container) | |
859 this._tree(child.container, options, child); | |
860 | |
861 parent.children.push (child); | |
862 } | |
863 | |
864 return parent; | |
865 }, | |
866 | |
867 tree: function(element) { | |
868 element = $(element); | |
869 var sortableOptions = this.options(element); | |
870 var options = Object.extend({ | |
871 tag: sortableOptions.tag, | |
872 treeTag: sortableOptions.treeTag, | |
873 only: sortableOptions.only, | |
874 name: element.id, | |
875 format: sortableOptions.format | |
876 }, arguments[1] || { }); | |
877 | |
878 var root = { | |
879 id: null, | |
880 parent: null, | |
881 children: [], | |
882 container: element, | |
883 position: 0 | |
884 }; | |
885 | |
886 return Sortable._tree(element, options, root); | |
887 }, | |
888 | |
889 /* Construct a [i] index for a particular node */ | |
890 _constructIndex: function(node) { | |
891 var index = ''; | |
892 do { | |
893 if (node.id) index = '[' + node.position + ']' + index; | |
894 } while ((node = node.parent) != null); | |
895 return index; | |
896 }, | |
897 | |
898 sequence: function(element) { | |
899 element = $(element); | |
900 var options = Object.extend(this.options(element), arguments[1] || { }); | |
901 | |
902 return $(this.findElements(element, options) || []).map( function(item) { | |
903 return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; | |
904 }); | |
905 }, | |
906 | |
907 setSequence: function(element, new_sequence) { | |
908 element = $(element); | |
909 var options = Object.extend(this.options(element), arguments[2] || { }); | |
910 | |
911 var nodeMap = { }; | |
912 this.findElements(element, options).each( function(n) { | |
913 if (n.id.match(options.format)) | |
914 nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; | |
915 n.parentNode.removeChild(n); | |
916 }); | |
917 | |
918 new_sequence.each(function(ident) { | |
919 var n = nodeMap[ident]; | |
920 if (n) { | |
921 n[1].appendChild(n[0]); | |
922 delete nodeMap[ident]; | |
923 } | |
924 }); | |
925 }, | |
926 | |
927 serialize: function(element) { | |
928 element = $(element); | |
929 var options = Object.extend(Sortable.options(element), arguments[1] || { }); | |
930 var name = encodeURIComponent( | |
931 (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); | |
932 | |
933 if (options.tree) { | |
934 return Sortable.tree(element, arguments[1]).children.map( function (item) { | |
935 return [name + Sortable._constructIndex(item) + "[id]=" + | |
936 encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); | |
937 }).flatten().join('&'); | |
938 } else { | |
939 return Sortable.sequence(element, arguments[1]).map( function(item) { | |
940 return name + "[]=" + encodeURIComponent(item); | |
941 }).join('&'); | |
942 } | |
943 } | |
944 }; | |
945 | |
946 // Returns true if child is contained within element | |
947 Element.isParent = function(child, element) { | |
948 if (!child.parentNode || child == element) return false; | |
949 if (child.parentNode == element) return true; | |
950 return Element.isParent(child.parentNode, element); | |
951 }; | |
952 | |
953 Element.findChildren = function(element, only, recursive, tagName) { | |
954 if(!element.hasChildNodes()) return null; | |
955 tagName = tagName.toUpperCase(); | |
956 if(only) only = [only].flatten(); | |
957 var elements = []; | |
958 $A(element.childNodes).each( function(e) { | |
959 if(e.tagName && e.tagName.toUpperCase()==tagName && | |
960 (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) | |
961 elements.push(e); | |
962 if(recursive) { | |
963 var grandchildren = Element.findChildren(e, only, recursive, tagName); | |
964 if(grandchildren) elements.push(grandchildren); | |
965 } | |
966 }); | |
967 | |
968 return (elements.length>0 ? elements.flatten() : []); | |
969 }; | |
970 | |
971 Element.offsetSize = function (element, type) { | |
972 return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; | |
973 }; |