prototype.js で Floating Leaflets

prototype.js を使って付箋紙風の DHTML を作ってみました。BSD ライセンスにしておきます。
https://tociyuki.sakura.ne.jp/test/postit.html
元ネタは、萩原 真一さんのJavaScript Tips collectionの「付箋紙」 Ver1.1です。
といっても、参考にしたのはアイデアとスタイルで、元ネタのスクリプトは読んでいません。
(12月3日。Post itは登録商標だったなと思い当たりましたので、名称変更しました。)
ブラウザ依存性を prototype.js が吸収してくれますので、記述が短くなり、かつ見通し良く、ロジックに集中できるのはありがたいことで、あっというまにできあがってしまいました。
ところで、元ネタに次の説明があるのですが、Firefox 1.5 と MSIE 6 で試した限りでは、innerHTML へ代入しても onmousemove イベントは通知されるようです。

http://www.din.or.jp/~hagi3/JavaScript/JSTips/DHTML/Samples/Husen.htm
これは、マウスドラッグとクリックの共存させた例です。
レイアのドキュメントを書換えると onmousemove イベントが通知されなくなるので、 付箋紙レイアの上に同じサイズのイベント取得用レイアを生成して実現しています。

なので、私の版では付箋紙の div 要素一層だけで済ませています。
クリックとドラッグの判定は、onmouseup 時と onmousedown 時のポインタの座標とのマンハッタン・ディスタンスが 2px 未満のときにクリックとみなすようにしてみました。ドラッグは、定石通り onmousemove イベントリスナで div を移動して実現しています。
クリックのときは、div の上に textarea を重ね表示してフォーカスを移しています。フォーカスを失うと編集終了とみなします。textarea.value のための値をLeafletインスタンスのプロパティ value に格納しておいて、それを編集前に textarea.value に格納してます。編集後にインスタンスのプロパティに取り出し、escapeHTML などの処理をしてから div 要素の innerHTML プロパティに格納します。編集前に、div 要素の innerHTML プロパティから textarea.value に移し、編集後に戻しています。その際に、br 要素を改行に変更する処理をおこなっています。
内容の保存機能は未実装ですが、_on_editend で Ajax.Request を使って post するようにすれば、簡単に書けるのでしょう。initialize で初期値をセットするようにすれば、保存しておいた内容を load イベントリスナで呼び出すこともできますね。そのうち書いて、 Perlwema サブセットにでも、してみましょうか。

Javascript部分

12月3日に手直し。

// LICENSE:
//   This is the free software under Berkeley Software Distribution License.
//   You can use, modify, and redistribute it without author's permissions.
// AUTHOR: MIZUTANI Tociyuki
// VERSION: 0.01

var Leaflet = Class.create();
Leaflet.init = function() {
  Leaflet.textarea = document.createElement('textarea');
  Leaflet.textarea.style.zIndex = 5000;
  Element.hide(Leaflet.textarea);
  document.body.appendChild(Leaflet.textarea);
  Leaflet.uniqid = 1;
  Leaflet._default = {
    width:'162px', height:'100px',
    color:'black', backgroundColor:'#ffffaa',
    value:'クリックで編集。\nドラッグ可能。\nサイズ変更と保存は未対応。'
  };
}; Leaflet.init();
Leaflet.prototype = {
  initialize: function(h) {
    this.createElement(h);
    this.downX = -10000; this.downY = -10000;
    this.mousemoveListner = this._on_mousemove.bindAsEventListener(this);
    this.mouseupListner = this._on_mouseup.bindAsEventListener(this);
    this.editendListner = this._on_editend.bindAsEventListener(this);
    Event.observe(this.el, 'mousedown',
      this._on_mousedown.bindAsEventListener(this), false);
  },
  createElement: function(h) {
    h = h || Leaflet._default;
    var div = document.createElement('div');
    div.id = 'pi' + Leaflet.uniqid;
    div.style.position = 'absolute';
    div.style.top = h.top || ((96 + Leaflet.uniqid * 8)+'px');
    div.style.left = h.left || ((32 + Leaflet.uniqid * 8)+'px');
    div.style.zIndex = (Leaflet.uniqid + 100)+'';
    div.style.width = h.width;
    div.style.height = h.height;
    div.style.color = h.color;
    div.style.backgroundColor = h.backgroundColor;
    div.style.overflow = 'hidden';
    div.style.padding = '3px';
    div.style.borderLeft = '1px solid #aaaaaa';
    div.style.borderTop  = '1px solid #aaaaaa';
    div.style.borderRight = '1px solid #777777';
    div.style.borderBottom = '1px solid #777777';
    document.body.appendChild(div);
    ++Leaflet.uniqid;
    this.el = div;
    this.setValue(h.value);
    Leaflet._default.value = '';
  },
  setValue: function(s) {
    this.value = s;
    this.el.innerHTML = this.value.escapeHTML().replace(/\n/g, &#39;<br />&#39;);
  },
  _on_mousedown: function(e) {
    var offsets = Position.cumulativeOffset(this.el);
    this.downX = Event.pointerX(e);
    this.downY = Event.pointerY(e);
    this.dx = offsets[0] - this.downX;
    this.dy = offsets[1] - this.downY;
    Event.observe(this.el, &#39;mousemove&#39;, this.mousemoveListner, false);
    Event.observe(this.el, &#39;mouseup&#39;, this.mouseupListner, false);
  },
  _on_mouseup: function(e) {
    Event.stopObserving(this.el, &#39;mousemove&#39;, this.mousemoveListner, false);
    Event.stopObserving(this.el, &#39;mouseup&#39;, this.mouseupListner, false);
    if ( Math.abs(this.downX - Event.pointerX(e)) < 2
      && Math.abs(this.downY - Event.pointerY(e)) < 2
    ) {
      this._on_editstart();
    }
  },
  _on_mousemove: function(e) {
    this.downX = -10000; this.downY = -10000;
    this.el.style.left = (Event.pointerX(e) + this.dx) + &#39;px&#39;;
    this.el.style.top = (Event.pointerY(e) + this.dy) + &#39;px&#39;;
  },
  _on_editstart: function() {
    Position.clone(this.el, Leaflet.textarea);
    Leaflet.textarea.value = this.value;
    Event.observe(Leaflet.textarea, &#39;blur&#39;, this.editendListner, false);
    Element.toggle(this.el, Leaflet.textarea);
    Field.focus(Leaflet.textarea);
  },
  _on_editend: function(e) {
    Element.toggle(this.el, Leaflet.textarea);
    this.setValue(Leaflet.textarea.value);
    Event.stopObserving(Leaflet.textarea, &#39;blur&#39;, this.editendListner, false);
  }
}
new Leaflet();