付箋紙もどき (localStorage 版 マルチタブ非対応)

2 年前に作った付箋紙もどきを localStorage で永続化するように変更しました。

https://tociyuki.sakura.ne.jp/archive/postit.html

https://tociyuki.sakura.ne.jp/archive/postit.js

以前は付箋紙に書き込んだ文字列をオブジェクトに並べていたのを変更し、 付箋紙一つにつき、位置、 重なり順、 文字列を記録するようにします。 zorder 配列は表示の下から上へ重なる表示順を格納してあります。 利用上の注意点としては、 複数のタブで同じページを開いているとき、 localStorage をタブで共有するため、 今のやりかたでは、 一方のタブの更新内容が一方の内容とは食い違ってしまう問題があります。

$(function(){
  // localStorage で使うキーの名前です
  var STORAGEKEY = 'clips-leaflets';

  // cards[divid] = {'left': absolute 位置の X px (Integer),
  //                 'top':  absolute 位置の Y px (Integer),
  //                 'zorder': zorder 配列中のインデックス (Integer),
  //                 'value': 入力文字列};
  // 下から div#bx3, div#bx1, div#bx2, div#bx4 の順に上へ重なります
  // zorder = ['#bx3', '#bx1', '#bx2', '#bx4'];
  var cards = {};
  var zorder = [];
  var orgX = parseInt($('#te').offset().left);
  var orgY = parseInt($('#te').offset().top);

  var escapehtml = function(s) {
    return s.replace(/&/g, '&').replace(/\x22/g, '"')
            .replace(/</g, '&lt;').replace(/>/g, '&gt;');
  };

//@<cards_restore@>
//@<cards_save@>
//@<card_create@>
//@<div_bringtofront@>
//@<div_mousedown@>
//@<div_mousemove@>
//@<div_mouseup@>
//@<textarea_blur@>
//@<checkStorageAvailable@>

  if (checkStorageAvailable('localStorage')) {
    cards_restore ();
    $(window).on('unload', cards_save);
  }
  $('#te').hide();
  $('#create').on('click', function(){
    var seq = zorder.length;
    card_create('#bx' + (seq + 1).toString(), orgX + seq * 2, orgY + seq * 2);
  });
});

localStorage に登録済みのときは、 JSON で格納してある cards オブジェクトをデコードし、 それらの値を使って zorder 配列に id をセットします。 登録がないときは、 textarea#te の内容から付箋紙オブジェクトを新しく作ります。

//@<cards_restore@>=
  var cards_restore = function() {
    if (localStorage.getItem(STORAGEKEY)) {
      cards = JSON.parse(localStorage.getItem(STORAGEKEY));
      for (var k in cards)
        zorder[cards[k].zorder] = k;
      for (var k in cards)
        card_create(k, cards[k].left, cards[k].top);
    }
    else {
      cards['#bx1'] = {'value': $('#te').val(), 'left': orgX, 'top': orgY};
      card_create('#bx1', orgX, orgY);
    }
  };

window の unload 時に、 zorder 配列内での順を cards オブジェクトに戻し、 JSONエンコードして localStorage に登録します。 「localStorage から削除」のチェックボックスがオンのときは、 localStorage から削除します。

//@<cards_save@>=
  var cards_save = function() {
    if ($('#remove').prop('checked')) {
      localStorage.removeItem(STORAGEKEY);
    }
    else {
      zorder.forEach(function (id, i, a) {
        cards[id].zorder = i;
      });
      localStorage.setItem(STORAGEKEY, JSON.stringify(cards));
    }
  };

load 時に localStorage から読み取った内容で付箋紙の div 要素を作ります。 また、 new ボタンを押したときも、 同じ関数で div 要素を作ります。 さらに、 付箋紙オブジェクトの重なり順を記録している zorder 配列の末尾に付箋紙オブジェクトの id をシャープ記号付きのセレクタ名で追加します。 zorder の末尾側が、 重なりでは手前側になるので、 末尾に追加することで最も手前に表示することになります。 div 要素のスタイルを更新し、 マウス・ボタンをユーザが押したときに実行するイベント・ハンドラを設定すると、 初期化は終わりです。

//@<card_create@>=
  var card_create = function(divid, initX, initY) {
    // divid は '#bx1' の書式です。 先頭のシャープ記号を除いて id 属性にセットします。
    $('body').append(
      $('<div></div>').attr('id', divid.substr(1)).attr('class', 'bx') );
    // divid の付箋紙オブジェクトが既にあるときは、 それを流用します。
    if (cards.hasOwnProperty(divid))
      $(divid).html(escapehtml(cards[divid].value).replace(/\n/g, '<br />'));
    else
      cards[divid] = {'value': '', 'left': initX, 'top': initY};
    // zorder にないときは、 末尾に追加して、 最も手前に表示します。
    if (zorder.indexOf(divid) == -1)
      zorder.push(divid);
    // スタイルとマウス・ボタン・ダウン時のイベント・ハンドラを設定します。
    $(divid)
      .css('position', 'absolute')
      .css('left', cards[divid].left.toString () + 'px')
      .css('top',  cards[divid].top.toString () + 'px')
      .css('z-index', (100 + zorder.indexOf(divid)).toString ())
      .on('mousedown', div_mousedown);
  };

マウスボタンが div 要素上で押されたら、 その要素を重なりの手前へ持ち上げます。 その後、 マウスの位置をイベントから取り出して、 移動追跡用とクリック判定用のそれぞれのハンドラに都合の良い座標値で値を受け渡します。

//@<div_mousedown@>=
  var div_mousedown = function(ev) {
    var divid = '#' + $(this).attr('id');
    div_bringtofront (divid);
    // ドラッグとクリック用のイベント・ハンドラを登録します。
    var downoffset = $(this).offset();
    var dx = downoffset.left - ev.pageX;
    var dy = downoffset.top  - ev.pageY;
    $(this).on('mousemove', {'dx': dx, 'dy': dy}, div_mousemove)
           .one('mouseup', {'downX': ev.pageX, 'downY': ev.pageY}, div_mouseup);
  };

要素を持ち上げるには、 要素の順番をあらわす zorder 配列を更新します。 一番手前へ移動するには、 zorder の途中から削除して、 末尾へ追加します。 その後、 zorder に従って div 要素の z-index スタイルを変更します。

//@<div_bringtofront@>=
  var div_bringtofront = function(divid) {
    zorder.splice(zorder.indexOf(divid), 1);
    zorder.push(divid);
    zorder.forEach(function (bxid, i, a) {
      $(bxid).css('z-index', (100 + i).toString());
    });
  };

マウスのボタンを押したまま移動すると、 div 要素をドラッグします。 付箋紙オブジェクトの位置を変更し、 それを div 要素の位置へ設定します。

//@<div_mousemove@>=
  var div_mousemove = function(ev) {
    var divid = '#' + $(this).attr('id');
    cards[divid].left = ev.pageX + ev.data.dx;
    cards[divid].top  = ev.pageY + ev.data.dy;
    // div 要素をマウスと一緒に動かして移動させます。
    $(this).css('left', cards[divid].left.toString () + 'px')
           .css('top',  cards[divid].top.toString () + 'px');
  };

マウスのボタンを離すと、 クリックかどうかを判定します。 ここでは、 ボタンを押した位置と離した位置が数ピクセル以内のとき、 クリックしたと判断します。 クリックのときは、 編集に入ります。 編集には textarea を使っており、 div 位置へ textarea を動かし、 blur イベントが発生するまで編集を続けてもらいます。

//@<div_mouseup@>=
  var div_mouseup = function(ev) {
    var divid = '#' + $(this).attr('id');
    $(this).off('mousemove');
    // クリックかどうかを判断します。
    var rx = Math.abs(ev.data.downX - ev.pageX);
    var ry = Math.abs(ev.data.downY - ev.pageY);
    if ((rx >= 2) || (ry >= 2))
      return;

    // ここから下がクリック処理です。
    var upoffset = $(this).offset();
    // div 要素を隠し、 その位置に textarea#te を移動して表示します。
    $(divid).hide();
    $('#te').width($(divid).width()).height($(divid).height())
      .css('left', upoffset.left.toString () + 'px')
      .css('top',  upoffset.top.toString () + 'px')
      .val(cards[divid].value)
      .show().focus()
      .one('blur', {'divid': divid}, textarea_blur);
  };

blur イベントで textarea から div 要素へ書き戻します。

//@<textarea_blur@>=
  var textarea_blur = function(ev) {
    var divid = ev.data.divid;
    cards[divid].value = $('#te').val();
    $(divid).html(escapehtml(cards[divid].value).replace(/\n/g, '<br />'));
    $('#te').hide();
    $(divid).show();
  };

window.localStorage があり、 setItem できるなら、 真を返します。

//@<checkStorageAvailable@>=
  var checkStorageAvailable = function(type) {
    try {
      var storage = window[type], x = '__storage_test__';
      storage.setItem(x, x);
      storage.removeItem(x);
      return true;
    }
    catch (e) {
      return false;
    }
  };