script.aculo.usを読み解く

第12回 dragdrop.js (中編)

この記事を読むのに必要な時間:およそ 12 分

0227:var Draggable = Class.create({
0228:  initialize: function(element) {
0229:    var defaults = {
0230:      handle: false,
0231:      reverteffect: function(element, top_offset, left_offset) {
0232:        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
0233:        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
0234:          queue: {scope:'_draggable', position:'end'}
0235:        });
0236:      },
0237:      endeffect: function(element) {
0238:        var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
0239:        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
0240:          queue: {scope:'_draggable', position:'end'},
0241:          afterFinish: function(){ 
0242:            Draggable._dragging[element] = false 
0243:          }
0244:        }); 
0245:      },
0246:      zindex: 1000,
0247:      revert: false,
0248:      quiet: false,
0249:      scroll: false,
0250:      scrollSensitivity: 20,
0251:      scrollSpeed: 15,
0252:      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
0253:      delay: 0
0254:    };
0255:    
0256:    if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
0257:      Object.extend(defaults, {
0258:        starteffect: function(element) {
0259:          element._opacity = Element.getOpacity(element);
0260:          Draggable._dragging[element] = true;
0261:          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
0262:        }
0263:      });
0264:    
0265:    var options = Object.extend(defaults, arguments[1] || { });
0266:
0267:    this.element = $(element);
0268:    
0269:    if(options.handle && Object.isString(options.handle))
0270:      this.handle = this.element.down('.'+options.handle, 0);
0271:    
0272:    if(!this.handle) this.handle = $(options.handle);
0273:    if(!this.handle) this.handle = this.element;
0274:    
0275:    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
0276:      options.scroll = $(options.scroll);
0277:      this._isScrollChild = Element.childOf(this.element, options.scroll);
0278:    }
0279:
0280:    Element.makePositioned(this.element); // fix IE    
0281:
0282:    this.options  = options;
0283:    this.dragging = false;   
0284:
0285:    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
0286:    Event.observe(this.handle, "mousedown", this.eventMouseDown);
0287:    
0288:    Draggables.register(this);
0289:  },
0290:  

227~290行目のinitializeは,インスタンスの初期化をする関数です。種々のプロパティは上述の通りです。

256~263行目で,デフォルトのstarteffectのフェードアウトは,デフォルトのendeffectのフェードインと一対になる必要があるので,ユーザが両方ともデフォルトから変更していないことを確かめてから,starteffectを追加しています。

269行目で,まずoptions.handleに,element要素の子要素が含むクラス名を与えた場合を扱います。

その場合,270行目で,Element.downメソッドで該当する要素を取得します。

272行目で,次に,options.handleにDOM idを与えた場合を扱います。

273行目で,何も与えていない場合は,element要素になります。

275~278行目で,options.scrollに,DOM idが与えられた場合は,$関数を適用し,さらに,それがelementの親要素かどうかを_isScrollChildに保存します。

280行目で,Element.makePositionedで,要素の移動に備えます。

286行目で,ハンドル(把手)の要素にmousedownイベントハンドラを設定します。

288行目で,Draggablesクラスに登録します。

0291:  destroy: function() {
0292:    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
0293:    Draggables.unregister(this);
0294:  },
0295:  

291~295行目のdestroyは,ドラッグ機能を解除する関数です。

292行目で,イベントハンドラを解除します。

293行目で,Draggablesクラスから削除します。

0296:  currentDelta: function() {
0297:    return([
0298:      parseInt(Element.getStyle(this.element,'left') || '0'),
0299:      parseInt(Element.getStyle(this.element,'top') || '0')]);
0300:  },
0301:  

296~301行目のcurrentDeltaは,現在のelement要素のCSSのleft,topプロパティの値を[x,y]の配列のかたちで返す関数です。

0302:  initDrag: function(event) {
0303:    if(!Object.isUndefined(Draggable._dragging[this.element]) &&
0304:      Draggable._dragging[this.element]) return;
0305:    if(Event.isLeftClick(event)) {    
0306:      // abort on form elements, fixes a Firefox issue
0307:      var src = Event.element(event);
0308:      if((tag_name = src.tagName.toUpperCase()) && (
0309:        tag_name=='INPUT' ||
0310:        tag_name=='SELECT' ||
0311:        tag_name=='OPTION' ||
0312:        tag_name=='BUTTON' ||
0313:        tag_name=='TEXTAREA')) return;
0314:        
0315:      var pointer = [Event.pointerX(event), Event.pointerY(event)];
0316:      var pos     = Position.cumulativeOffset(this.element);
0317:      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
0318:      
0319:      Draggables.activate(this);
0320:      Event.stop(event);
0321:    }
0322:  },
0323:  

302~323行目のinitDragは,handle要素のmousedownイベントハンドラで,ドラッグの開始直前に呼ばれる関数です。

303行目で,Draggable._draggingを調べて,本当にドラッグの直前かどうかを確かめます。

Firefoxでは,フォームを構成する要素はドラッグ&ドロップに対応しません。306~313行目で,要素のタグ名を調べて,input,select,option,button,textareaの要素であれば処理を中止します。

317行目で,offsetは,ドラッグを開始したときの,マウスポインタの位置と要素の位置のズレです。

319行目で,Draggables.activateで,この要素を現在ドラッグ中とします。

0324:  startDrag: function(event) {
0325:    this.dragging = true;
0326:    if(!this.delta)
0327:      this.delta = this.currentDelta();
0328:    
0329:    if(this.options.zindex) {
0330:      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
0331:      this.element.style.zIndex = this.options.zindex;
0332:    }
0333:    
0334:    if(this.options.ghosting) {
0335:      this._clone = this.element.cloneNode(true);
0336:      this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
0337:      if (!this.element._originallyAbsolute)
0338:        Position.absolutize(this.element);
0339:      this.element.parentNode.insertBefore(this._clone, this.element);
0340:    }
0341:    
0342:    if(this.options.scroll) {
0343:      if (this.options.scroll == window) {
0344:        var where = this._getWindowScroll(this.options.scroll);
0345:        this.originalScrollLeft = where.left;
0346:        this.originalScrollTop = where.top;
0347:      } else {
0348:        this.originalScrollLeft = this.options.scroll.scrollLeft;
0349:        this.originalScrollTop = this.options.scroll.scrollTop;
0350:      }
0351:    }
0352:    
0353:    Draggables.notify('onStart', this, event);
0354:        
0355:    if(this.options.starteffect) this.options.starteffect(this.element);
0356:  },
0357:  

324~357行目のstartDragは,ドラッグの開始時に呼ばれる関数です。はじめてupdateDragが呼ばれたときに呼ばれます。

325行目で,ドラッグ中であることを示すdraggingフラグをたてます。

326行目で,deltaは,ドラッグの出発地点を表します。この要素にとってドラッグでの移動が初めてであれば,ここで現在の位置を出発地点とします。

329~333行目で,options.zindexの設定がある場合は,originalZに要素のCSSのz-indexプロパティの元の値を保存してから,値を書きかえます。

334~341行目で,options.ghostingの設定がある場合は,element要素のクローンを作り,それを要素のDOMツリーの直前に挿入します。その際に表示が乱れないよう,Position.absolutizeで要素をフローから外します。このとき,要素のCSSのpositionプロパティの元の値を_originallyAbsoluteに保存しておきます。

342~352行目で,originalScrollTop,同Leftを設定します。options.scrollがwindowであればページのスクロールの位置を,そうでなければoptions.scrollの要素のスクロールの位置とします。

353行目で,Draggables.notifyで,'onStart'関係のフックを呼びます。

355行目で,options.starteffectフックがあれば呼びます。

著者プロフィール

源馬照明(げんまてるあき)

名古屋大学大学院多元数理科学研究科1年。学部生のときにSchemeの素晴らしさを知ったのをきっかけに,関数型言語の世界へ。JavaScriptに,ブラウザからすぐに試せる関数型言語としての魅力と将来性を感じている。

ブログ:Gemmaの日記