annotate public/javascripts/dragdrop.js @ 8:0c83d98252d9 yuya

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