HaH_modoki (Hit-a-Hint風greasemonkey)

atsumu-t2009-04-10

追記 2009-04-21 01:51

window.openをGM_openInTabに変えたので、ポップアップブロックに引っかからなくなったか引っかかりにくくなったはずです。

追記

忘れるのでメモ。
偶数文字目と奇数文字目に使う文字を別々に指定できるようにすると、子音字が連続することを防げるので、ローマ字入力の人にとって入力しやすくなる。

概要

キーボード操作のみでリンクを開くためのgreasemonkeyです。
http://d.hatena.ne.jp/javascripter/20080531/1212190340 を元にしました。

document.link.hasOwnPropertyが呼び出せなくてイヤになったので、書きかけですが公開します。
firefox3以降で動きます。
たまにしかJavaScriptを書かないので、変なところがあったらコメントお願いします。

特徴

  • フォームにフォーカスを移せる。
  • フォームにフォーカスがあるときは動かない。
  • フォームからフォーカスをはずすキーを割り当てれる。
  • リンクの選択に使うキー・使わないキーを自由に選べる。
  • 新しいウィンドウで開くかどうかを入力中にトグルするキーを割り当てれる。
  • 最後にEnterを押さなくていい。

実はHit-a-Hintとか使ったことがないので差がわかりません。

マニュアル

先頭付近にある変数oのプロパティがオプションです。

プロパティ 説明
start_keys リンクの選択を開始するキー
cancel_keys リンクの選択をキャンセルして終了するキー
input_keys リンクの選択に使用するキー
shift_is_new_window shift状態のときに新しいウィンドウで開くかどうか
shift_on_keys shift状態にするキー
shift_off_keys shift状態をやめるキー
shift_toggle_keys shift状態をトグルするキー
blur_keys フォーカスをはずすキー
tip_offset_h tipの表示位置のオフセット(縦)
tip_offset_v tipの表示位置のオフセット(横)
tip_class tipに割り当てるクラス名。他のグリモンとかぶってたら変える

どの機能にも複数のキーを指定できます。

液晶が届いたので、残りは後で書きます。

ソースコード

// ==UserScript==
// @name           HaH_modoki
// @namespace      http://d.hatena.ne.jp/atsumu-t/
// @include        *
// ==/UserScript==

var ESC = String.fromCharCode (27);
var o = {
  start_keys: 'o',
  cancel_keys: 'o' + ESC,
  input_keys: 'abcdefghjklmnprstuvwxy',
  shift_is_new_window: false,
  shift_on_keys: '',
  shift_off_keys: '',
  shift_toggle_keys: 'i',
  blur_keys: '' + ESC,
  
  tip_offset_h: 0,
  tip_offset_v: 0,
  
  tip_class: '_HaHm_tip',
};

function makeTipTemplate () {
  var t = document.createElement ('span');
  t.className = o.tip_class;
  with (t.style) {
    color = 'white';
    backgroundColor = 'rgba(0, 0, 0, 0.5)';
    paddingLeft = '0.2em';
    paddingRight = '0.2em';
    borderColor = 'white';
    borderStyle = 'solid';
    borderWidth = '0.1em';
    
    zIndex = '999999';
    position = 'absolute';
  }
  return t;
}

var g = {
  objs: new Array (),
  tip_digit: new Number (),
  enable: false,
  s: new State (),
};

function State () {
  this.shift = false;
  this.input = new String ();
}

function open (href, shift) {
  if ((o.shift_is_new_window && shift)
      || (!o.shift_is_new_window && !shift))
    //window.open (href, '_blank', '');
    GM_openInTab (href);
  else
    location.href = href;
}

function log (x, base) {
  return Math.log (x) / Math.log (base);
}

function key_match (e, s) {
  return (s.indexOf (String.fromCharCode (e.which)) != -1
          || s.indexOf (String.fromCharCode (e.keyCode)) != -1);
}

function keypress (e) {
  {
    var elem = e.target;
    var elem_name = elem.nodeName.toLowerCase ();
    
    if (key_match (e, o.blur_keys)) {
      elem.blur ();
      if (g.enable) {
        removeTip ();
        g.enable = false;
      }
    }
    if (elem_name == 'textarea'
        || elem_name == 'select'
        || (elem_name == 'input'
            && (elem.type == 'text' || elem.type == 'password'))
        || e.ctrlKey || e.altKey || e.button)
      return true;
  }

  var c = String.fromCharCode (e.which);
  if (!g.enable) {
    if (key_match (e, o.start_keys)) {
      if (!showTip ())
        return true;
      g.enable = true;
      g.s = new State ();
    }
  } else {
    if (key_match (e, o.cancel_keys)) {
      removeTip ();
      g.enable = false;
    } else if (key_match (e, o.shift_on_keys)) {
      g.s.shift = true;
    } else if (key_match (e, o.shift_off_keys)) {
      g.s.shift = false;
    } else if (key_match (e, o.shift_toggle_keys)) {
      g.s.shift = !g.s.shift;
    } else if (key_match (e, o.input_keys)) {
      g.s.input += c;
      var tips = document.getElementsByClassName ('_HaHm_tip');
      
      var removes = [];
      for (var i = 0; i < tips.length; ++i) {
        if (tips.item(i).textContent.indexOf (c) == 0)
          tips.item(i).style.color = 'yellow';
        else
          removes.push (i);
      }
      if (removes.length == tips.length
          && g.s.input.length != g.tip_digit) {
        removeTip ();
        g.enable = false;
        return false;
      }
      for (var i = 0; i < removes.length; ++i) {
        var item = tips.item(removes[i] - i);
        item.parentNode.removeChild (item);
      }
    }

    if (g.s.input.length == g.tip_digit) {
      removeTip ();
      g.enable = false;
      var n = 0;
      for (var i = 0; i < g.tip_digit; ++i) {
        n = n * o.input_keys.length + o.input_keys.indexOf (g.s.input[i]);
      }
      if (n < g.objs.length)
        {
          if (g.objs[n].proc == 'open')
            open (g.objs[n].elem.href, g.s.shift);
          else if (g.objs[n].proc == 'focus')
            g.objs[n].elem.focus ();
        }
    }
  }
  return false;
}

function showTip () {
  var template = makeTipTemplate ();

  var w_left = window.scrollX;
  var w_right = window.scrollX + window.innerWidth;
  var w_top = window.scrollY;
  var w_bottom = window.scrollY + window.innerHeight;
  var w_width = w_right - w_left;
  var w_height = w_bottom - w_top;
  
  g.objs = new Array ();
  Array.filter (
    document.links,
    function (l) {
      var pos = l.getBoundingClientRect ();
      if (0 <= pos.left && pos.right <= w_width
          && 0 <= pos.top && pos.bottom <= w_height)
        g.objs.push ({elem: l, proc: 'open'});
      return true;
    });

  {
    var inputs = document.getElementsByTagName ("input");
    var textareas = document.getElementsByTagName ("textarea");
    var selects = document.getElementsByTagName ("select");
    var as = new Array (inputs.length + textareas.length + selects.length);
    for (var i = 0; i < inputs.length; ++i)
      as[i] = inputs[i];
    for (var i = 0; i < textareas.length; ++i)
      as[inputs.length + i] = textareas[i];
    for (var i = 0; i < selects.length; ++i)
      as[inputs.length + textareas.length + i] = selects[i];

    Array.filter (
      as,
      function (a) {
        var pos = a.getBoundingClientRect ();
        if (0 <= pos.left && pos.right <= w_width
            && 0 <= pos.top && pos.bottom <= w_height)
          g.objs.push ({elem: a, proc: 'focus'});
        return true;
      });
  }

  if (g.objs.length == 0)
    return false;

  g.tip_digit = (g.objs.length == 0 ? 0
                 : Math.floor (log (g.objs.length, o.input_keys.length)) + 1);

  g.objs.forEach (
    function (link, i) {
      var l = link.elem;
      var pos = l.getBoundingClientRect ();
      var tip = template.cloneNode (true);
      var str = "";
      var rest = i;
      for (var d = g.tip_digit; d > 0; --d) {
        str = o.input_keys[rest % o.input_keys.length] + str;
        rest = Math.floor (rest / o.input_keys.length);
      }
      with (tip) {
        textContent = str;
        style.left = pos.left + w_left + o.tip_offset_h * g.tip_digit + 'px';
        style.top = pos.top + w_top + o.tip_offset_v + 'px';
      }
      document.body.appendChild (tip);
    });

  return true;
}

function removeTip () {
  var tips = document.getElementsByClassName (o.tip_class);
  while (tips.length)
    tips[0].parentNode.removeChild (tips[0]);
}

document.addEventListener ('keypress', keypress, false);