Safari で navigate する

SafariOpera のような navigate をしたかったので書きました。(http://d.hatena.ne.jp/mzp/20081121 に触発されて)
それから、id:hiru926 が navigate の次の行き先を表示できれば使いやすいのに、と言っていたので適当に実装してみました。shift キーを押している間だけ変なフレームがでます。
現状では先読みが見にくかったり navigate がやっぱり微妙に変だったりするので、まだいろいろと改善の余地ありかなと。

// ==UserScript==
// @name         navigate.js
// @namespace    http://d.hatena.ne.jp/zyxwv/
// @include      *
// ==/UserScript==

(function (){
   const minimumWidth  = 100;
   const minimumHeight = 60;
   const borderWidth   = 3;

   var d = document;
   var xlinks = [];
   var ylinks = [];
   var currentTarget;
   var frame;
   var subframes = [];

   function prepareLinks() {
      function collectTags(tagname) {
	 var as = d.getElementsByTagName(tagname);
	 var counter = xlinks.length;
	 for (var i=0; i < as.length ; i++) {
	    if( tagname != 'a' || as[i].hasAttribute('href') ) {
	       var xy = Position.cumulativeOffset(as[i]);
	       var xlow, xhigh, ylow, yhigh;
	       if ( as[i].offsetHeight < minimumHeight ) {
		  ylow  = xy[1] + as[i].offsetHeight/2 - minimumHeight/2;
		  yhigh = xy[1] + as[i].offsetHeight/2 + minimumHeight/2;
	       } else {
		  ylow  = xy[1];
		  yhigh = xy[1] + as[i].offsetHeight;
	       }
	       if ( as[i].offsetWidth < minimumWidth ) {
		  xlow  = xy[0] + as[i].offsetWidth/2 - minimumWidth/2;
		  xhigh = xy[0] + as[i].offsetWidth/2 + minimumWidth/2;
	       } else {
		  xlow  = xy[0];
		  xhigh = xy[0] + as[i].offsetWidth;
	       }
	       xlinks[counter] = {dom:as[i], index:xy[0], low:ylow, high:yhigh};
	       ylinks[counter] = {dom:as[i], index:xy[1], low:xlow, high:xhigh};
	       counter++;
	    }
	 }
      }
      collectTags('a');
      collectTags('input');
      collectTags('object');
      xlinks.sort(function (a,b){ return a.index - b.index });
      ylinks.sort(function (a,b){ return a.index - b.index });
   }
   function isInWindow(dom){
      xy = Position.cumulativeOffset(dom);
      return !(xy[1] > window.pageYOffset + window.innerHeight ||
	       xy[1] + dom.offsetHeight < window.pageYOffset   ||
	       xy[0] > window.pageXOffset + window.innerWidth  ||
	       xy[0] + dom.offsetWidth < window.pageXOffset)
   }
   function setupForcus(dom) {
      dom.focus();
      if (!isInWindow(dom)) {
	 var pos = Position.cumulativeOffset(dom)[1]-window.innerHeight/2;
	 window.scrollTo(0, pos);
      }
   }
   function findIndexOfTarget(list) {
      var pos = 0;
      while (list[pos].dom != currentTarget.dom) {
	 pos++;
      }
      return pos;
   }
   function setCurrentTarget(target) {
      function setFrame(dom){
	 xy = Position.cumulativeOffset(dom);
	 frame.style.display="block";
	 frame.style.width=(dom.offsetWidth+8)+"px";
	 frame.style.height=(dom.offsetHeight+8)+"px";
	 frame.style.top=xy[1]-4-borderWidth+"px";
	 frame.style.left=xy[0]-4-borderWidth+"px";
      }

      setupForcus(target.dom);
      setFrame(target.dom);
      currentTarget = target;
      setSubTarget();
   }
   function setSubTarget() {
      function setSubFrame(i){
	 return function(target) {
	    dom = target.dom;
	    xy = Position.cumulativeOffset(dom);
	    subframes[i].style.display="block";
	    subframes[i].style.width=(dom.offsetWidth+8)+"px";
	    subframes[i].style.height=(dom.offsetHeight+8)+"px";
	    subframes[i].style.top=xy[1]-4-borderWidth+"px";
	    subframes[i].style.left=xy[0]-4-borderWidth+"px";
	 }
      }

      pos = findIndexOfTarget(ylinks);
      findNextNode(1, pos, ylinks.length, ylinks,
		   currentTarget.dom.offsetHeight, setSubFrame(0)); // down
      findNextNode(-1, pos, ylinks.length, ylinks,
		   currentTarget.dom.offsetHeight, setSubFrame(1)); // up
      pos = findIndexOfTarget(xlinks);
      findNextNode(1, pos, xlinks.length, xlinks,
		   currentTarget.dom.offsetWidth, setSubFrame(2));  // right
      findNextNode(-1, pos, xlinks.length, xlinks,
		   currentTarget.dom.offsetWidth, setSubFrame(3));  // left
   }
   function findNextNode(direction, start, limit, list, minVariation, whenFound){
      whenFound = whenFound || setCurrentTarget;
      // setup current-target data
      currentTarget = list[findIndexOfTarget(list)];

      for (var pos=start; 0 < pos && pos < limit; pos+=direction) {
	 if (currentTarget.low < list[pos].high &&
	     list[pos].low < currentTarget.high &&
	     (Math.abs(list[pos].index-currentTarget.index) >= minVariation)) {
		// TODO: it should search some more candidates
		whenFound(list[pos]);
		return;
	    }
      }
      // default
      whenFound(list[pos]);
      return;
   }
   function displaySubFrames() {
      for (var i=0;i<4;i++) {
	 subframes[i].style.display = 'block';
      }
   }
   function hideSubFrames() {
      for (var i=0;i<4;i++) {
	 subframes[i].style.display = 'none';
      }
   }

   function navigateDOWN() {
      if (currentTarget == undefined) {
	 setupForcus(ylinks[0].dom);
	 currentTarget = ylinks[0];
      } else {
	 var direction = 1;
	 var pos = findIndexOfTarget(ylinks) + direction;
	 findNextNode(direction, pos, ylinks.length, ylinks,
		      currentTarget.dom.offsetHeight);
      }
   }
   function navigateUP() {
      if (currentTarget == undefined) {
	 setupForcus(ylinks[0].dom);
	 currentTarget = ylinks[0];
      } else {
	 var direction = -1;
	 var pos = findIndexOfTarget(ylinks) + direction;
	 findNextNode(direction, pos, ylinks.length, ylinks,
		      currentTarget.dom.offsetHeight);
      }
   }
   function navigateRIGHT() {
      if (currentTarget == undefined) {
	 setupForcus(xlinks[0].dom);
	 currentTarget = xlinks[0];
      } else {
	 var direction = 1;
	 var pos = findIndexOfTarget(xlinks) + direction;
	 findNextNode(direction, pos, xlinks.length, xlinks,
		      currentTarget.dom.offsetWidth);
      }
   }
   function navigateLEFT() {
      if (currentTarget == undefined) {
	 setupForcus(xlinks[0].dom);
	 currentTarget = xlinks[0];
      } else {
	 var direction = -1;
	 var pos = findIndexOfTarget(xlinks) + direction;
	 findNextNode(direction, pos, xlinks.length, xlinks,
		      currentTarget.dom.offsetWidth);
      }
   }

   function setupKeyBinding(){
      // handle key input
      const KeyEvent = {
	 DOM_VK_LEFT: 37,
	 DOM_VK_UP: 38,
	 DOM_VK_RIGHT: 39,
	 DOM_VK_DOWN: 40
      };
      var isShiftPressed = false;

      document.addEventListener('keydown', function (e){
	 var active = d.activeElement;
	 if ( active != undefined && active.tagName == 'INPUT' ) {
	 // no move
	 } else {
	    if( e.shiftKey ) {
	       isShiftPressed = true;
	       displaySubFrames();

	       switch(e.keyCode) {
		  case KeyEvent.DOM_VK_UP:
		  navigateUP();
		  e.preventDefault();
		  break;
		  case KeyEvent.DOM_VK_LEFT:
		  navigateLEFT();
		  e.preventDefault();
		  break;
		  case KeyEvent.DOM_VK_RIGHT:
		  navigateRIGHT();
		  e.preventDefault();
		  break;
		  case KeyEvent.DOM_VK_DOWN:
		  navigateDOWN();
		  e.preventDefault();
		  break;
		  default:
		  break;
	       }
	    }
	 }
      },true);
      document.addEventListener('keyup', function(e) {
	 if (isShiftPressed && !e.shiftKey) {
	    hideSubFrames();
	 }
      }, true);
   }
   function init() {
      frame = document.createElement('div');
      frame.style.border= borderWidth + "px solid royalblue";
      frame.style.position="absolute";
      frame.style.zindex="255";
      document.body.appendChild(frame);

      for(var i=0;i<4;i++) {
	 subframes[i] = document.createElement('div');
	 subframes[i].style.border= borderWidth + "px solid yellow";
	 switch (i) {
	    case 0: // down
	    subframes[i].style.borderTop = (borderWidth*2)+"px solid red"
	    break;
	    case 1: // up
	    subframes[i].style.borderBottom = (borderWidth*2)+"px solid red"
	    break;
	    case 2: // right
	    subframes[i].style.borderLeft = (borderWidth*2)+"px solid red"
	    break;
	    case 3: // left
	    subframes[i].style.borderRight = (borderWidth*2)+"px solid red"
	    break;
	 }
	 subframes[i].style.position="absolute";
	 subframes[i].style.zindex="255";
	 document.body.appendChild(subframes[i]);
      }
   }

   // NOW, START!
   init();
   prepareLinks();
   setupKeyBinding();
}())