Skip to content

Instantly share code, notes, and snippets.

@Griever
Created July 9, 2010 09:24
Show Gist options
  • Save Griever/469269 to your computer and use it in GitHub Desktop.
Save Griever/469269 to your computer and use it in GitHub Desktop.
Firefox で空間ナビゲーションを再現する uc.js
// ==UserScript==
// @name CrossFireModoki.uc.js
// @description Opera の空間ナビゲーションのまねごと
// @namespace http://d.hatena.ne.jp/Griever/
// @author Griever
// @license MIT License
// @compatibility Firefox 16
// @charset UTF-8
// @include main
// @version 0.0.1
// ==/UserScript==
/*
Windowsキー+カーソルキーで動く。
※対応未定
フォーカス時のハイライト(とりあえず選択状態にした)
フレームまたぎ
img[onclick] などブラウザのフォーカスが当たらないもの
*/
(function(){
"use strict";
if (window.gCrossFireModoki) {
window.gCrossFireModoki.destroy();
delete window.gCrossFireModoki;
}
window.gCrossFireModoki = {
DEBUG: false,
SELECTOR: [
':-moz-any-link'
, 'textarea:enabled:not([readonly])'
, 'input:enabled:not([readonly])'
, 'button:enabled:not([readonly])'
, 'select:enabled:not([readonly])'
, ':-moz-any-link img'
].join(","),
get flasher() {
var flasher = Cc["@mozilla.org/inspector/flasher;1"].createInstance(Ci.inIFlasher);
flasher.color = "#3D81EF";
flasher.thickness = 2;
delete this.flasher;
return this.flasher = flasher;
},
getFocusedWindow: function() {
var win = document.commandDispatcher.focusedWindow;
return (!win || win == window) ? window.content : win;
},
init: function() {
gBrowser.mPanelContainer.addEventListener("keypress", this, true);
window.addEventListener("unload", this, false);
},
uninit: function() {
gBrowser.mPanelContainer.removeEventListener("keypress", this, true);
window.removeEventListener("unload", this, false);
},
destroy: function() {
this.uninit();
if (this.xulstyle) this.xulstyle.parentNode.removeChild(this.xulstyle);
},
handleEvent: function(event) {
switch(event.type){
case "keypress":
if (!event.getModifierState("OS")) return; // Winキーが押されていなければ終了
if (event.ctrlKey || event.shiftKey || event.altKey) return;
var dir = null;
switch(event.keyCode) {
case event.DOM_VK_WIN:
event.preventDefault(); // Winキーが無効にできない。・゚・(ノД`)・゚・。
event.stopPropagation();
return
case event.DOM_VK_UP:
dir = "up";
break;
case event.DOM_VK_RIGHT:
dir = "right";
break;
case event.DOM_VK_DOWN:
dir = "down";
break;
case event.DOM_VK_LEFT:
dir = "left";
break;
}
if (!dir) return;
event.preventDefault();
event.stopPropagation();
this.find(dir, event.view);
break;
case "unload":
this.uninit();
break;
}
},
find: function(aDir, aWin) {
var win = aWin || this.getFocusedWindow();
var doc = win.document;
if (doc instanceof XULDocument || doc.body instanceof HTMLFrameSetElement)
return;
var isPrev = aDir === "up" || aDir === "left";
var inWidth = win.innerWidth;
var inHeight = win.innerHeight;
var cRect = null;
var currentNode = doc.activeElement;
if (currentNode != doc.body) {
cRect = getFirstLineRect(currentNode, isPrev);
} else {
var sel = win.getSelection();
if (sel.rangeCount)
cRect = getRect(sel.getRangeAt(0));
}
if (!cRect || cRect.top > inHeight || cRect.right < 0 || cRect.bottom < 0 || cRect.left > inWidth) {
cRect = this.createStartRect(win, aDir);
}
// ここから頭痛いゾーン
var aX, aY, aT, aR, aB, aL;// nodesFromRect の引数用
var _top, _right, _bottom, _left, _width, _height, _centerX, _centerY;// cRect を回した視点から見るための変数
switch(aDir) {
case "up":
[aX, aY, aT, aR, aB, aL] = [0, 0, 0, inWidth, cRect.top - 1, 0];
[_top, _right, _bottom, _left, _width, _height, _centerX, _centerY] =
["top", "right", "bottom", "left", "width", "height", "centerX", "centerY"];
break;
case "right":
[aX, aY, aT, aR, aB, aL] = [cRect.right + 1, 0, 0, inWidth - cRect.right, inHeight, 0];
[_top, _right, _bottom, _left, _width, _height, _centerX, _centerY] =
["right", "bottom", "left", "top", "height", "width", "centerY", "centerX"];
break;
case "down":
[aX, aY, aT, aR, aB, aL] = [0, cRect.bottom + 1, 0, inWidth, inHeight - cRect.bottom, 0];
[_top, _right, _bottom, _left, _width, _height, _centerX, _centerY] =
["bottom", "left", "top", "right", "width", "height", "centerX", "centerY"];
break;
case "left":
[aX, aY, aT, aR, aB, aL] = [0, 0, 0, cRect.left - 1, inHeight, 0];
[_top, _right, _bottom, _left, _width, _height, _centerX, _centerY] =
["left", "top", "right", "bottom", "height", "width", "centerY", "centerX"];
break;
}
var nodelist = this.nodesFromRect(win, aX, aY, aT, aR, aB, aL);
var data = [];
var withCurrent = distance.bind(this, cRect[_centerX], cRect[_top]);
nodelist.forEach(function(node){
if (node === currentNode) return;
var flag = node instanceof HTMLElement && node.mozMatchesSelector(this.SELECTOR);
if (!flag) return;
var rect = getRect(node);
if (rect.width < 1 || rect.height < 1) return;
// 対象がほぼ真横なら探さない。要改善
if (isPrev && rect[_centerY] > cRect[_centerY]) return;
if (!isPrev && rect[_centerY] < cRect[_centerY]) return;
rect.element = node;
var sa = Math.abs(cRect[_top] - rect[_bottom]);
var ds = [];
// ---- -- 現在地の真上に対象がある場合
// -- ---- 真上にリンクが複数並ぶとダブるで、中心との距離で差をつける
if (rect[_left] >= cRect[_left] && rect[_right] <= cRect[_right] ||
rect[_left] <= cRect[_left] && rect[_right] >= cRect[_right])
{
ds.push(sa + withCurrent(rect[_centerX], rect[_bottom]) / 100);
}
// ---- ---- 現在地の真上に対象の左端か右端があるとき
// ---- ----
else if (isPrev && rect[_left] > cRect[_left] && rect[_left] < cRect[_right] ||
!isPrev && rect[_right] > cRect[_right] && rect[_right] < cRect[_left] ||
isPrev && rect[_right] > cRect[_left] && rect[_right] < cRect[_right] ||
!isPrev && rect[_left] > cRect[_right] && rect[_left] < cRect[_left])
{
ds.push(sa + withCurrent(rect[_centerX], rect[_bottom]) / 100);
}
if (ds.length) {
rect.distance = ds[0]; //Math.min.apply(null, ds);
data.push(rect);
return;
}
// 現在地から斜め45°開いた場所に対象がある場合
// 左下が範囲内の場合
if (rect[_left] > cRect[_left] - sa && rect[_left] < cRect[_right] + sa) {
ds.push( withCurrent(rect[_left], rect[_bottom]) );
}
// 右下
if (rect[_right] > cRect[_left] - sa && rect[_right] < cRect[_right] + sa) {
ds.push( withCurrent(rect[_right], rect[_bottom]) );
}
sa = Math.abs(cRect[_top] - rect[_top]);
// 左上
if (rect[_left] > cRect[_left] - sa && rect[_left] < cRect[_right] + sa) {
ds.push( withCurrent(rect[_left], rect[_top]) );
}
// 右上
if (rect[_right] > cRect[_left] - sa && rect[_right] < cRect[_right] + sa) {
ds.push( withCurrent(rect[_right], rect[_top]) );
}
if (ds.length) {
rect.distance = Math.min.apply(null, ds) * 1.1; // 補正
data.push(rect);
}
}, this);
// 対象が見つからないので半画面スクロールして終了
if (!data.length) {
this.scrollHalf(win, aDir);
return
}
// 一番近い要素を探す
var nRect = null;
data.forEach(function(rect){
if (!nRect || nRect.distance > rect.distance) {
nRect = rect;
}
}, this);
if (nRect) {
this.focus(nRect.element, win);
}
},
focus: function(elem, aWin) {
if (elem instanceof HTMLImageElement) {
let e = getAncestorLink(elem);
if (e) e.focus();
} else {
elem.focus();
}
var doc = elem.ownerDocument;
var range = doc.createRange();
range.selectNode(elem);
var sel = doc.defaultView.getSelection();
sel.removeAllRanges();
sel.addRange(range);
//var win = aWin || this.getFocusedWindow();
//var self = this;
//win.setTimeout(function() {
// self.flasher.drawElementOutline(elem);
//}, 50);
},
scrollHalf: function(aWin, aDir) {
// 雑すぎる┐(´ー`)┌
switch(aDir) {
case "up":
aWin.scrollBy(0, -aWin.innerHeight / 2);
break;
case "right":
aWin.scrollBy(aWin.innerWidth / 2, 0);
break;
case "down":
aWin.scrollBy(0, aWin.innerHeight / 2);
break;
case "left":
aWin.scrollBy(-aWin.innerWidth / 2, 0);
break;
}
},
nodesFromRect: function(win, aX, aY, aTop, aRight, aBottom, aLeft) {
// http://piro.sakura.ne.jp/latest/blosxom/mozilla/xul/2010-07-07_nodesfromrect.htm
// nsIDOMNodeList nodesFromRect(in float aX,
// in float aY,
// in float aTopSize,
// in float aRightSize,
// in float aBottomSize,
// in float aLeftSize,
// in boolean aIgnoreRootScrollFrame,
// in boolean aFlushLayout);
var nodelist = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
.nodesFromRect(aX, aY, aTop, aRight, aBottom, aLeft, true, false);
if (false) {
var d = win.document.createElement("div");
var s = d.style;
s.position = "absolute";
s.zIndex = "20000";
s.backgroundColor = "rgba(255,255,0,.2)";
s.outline = "1px solid blue";
s.top = win.scrollY + aY + "px";
s.left = win.scrollX + aX + "px";
s.width = aRight + "px";
s.height = aBottom + "px";
win.document.body.appendChild(d);
d.setAttribute("onclick", "this.parentNode.removeChild(this);");
}
return $A(nodelist);
},
createStartRect: function(aWin, aDir) {
var rect = null;
switch(aDir) {
case "up":
rect = {
top : aWin.innerHeight,
right : aWin.innerWidth,
bottom : aWin.innerHeight,
left : 0,
};
break;
case "right":
rect = {
top : 0,
right : 0,
bottom : aWin.innerHeight,
left : 0,
};
break;
case "down":
rect = {
top : 0,
right : aWin.innerWidth,
bottom : 0,
left : 0,
};
break;
case "left":
rect = {
top : 0,
right : aWin.innerWidth,
bottom : aWin.innerHeight,
left : aWin.innerWidth,
};
break;
}
rect.width = rect.right - rect.left;
rect.height = rect.bottom - rect.top;
return parseRect(rect);
},
parseRect: parseRect,
getRect: getRect,
getFirstLineRect: getFirstLineRect,
distance: distance,
getAncestorLink: getAncestorLink,
};
window.gCrossFireModoki.init();
function distance(x1, y1, x2, y2) {
return Math.sqrt( Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2) );
}
function getRect(elem) {
var rect = parseRect(elem.getBoundingClientRect());
if (elem instanceof HTMLAnchorElement && elem.href) {
let images = elem.getElementsByTagName("img");
if (images.length) {
$A(images).forEach(function(img){
let r = img.getBoundingClientRect();
if (rect.top > r.top) rect.top = r.top;
if (rect.right < r.right) rect.right = r.right;
if (rect.bottom < r.bottom) rect.bottom = r.bottom;
if (rect.left > r.left) rect.left = r.left;
rect.width = rect.right - rect.left;
rect.height = rect.bottom - rect.top;
}, this);
}
}
return parseRect(rect);
}
function getFirstLineRect(elem, isLast) {
if (elem instanceof HTMLAnchorElement && elem.href) {
let img = elem.getElementsByTagName("img")[0];
if (img)
return getRect(elem);
}
var rects = $A(elem.getClientRects());
if (!isLast)
rects.reverse();
for (var i = 0, len = rects.length; i < len; ++i) {
if (rects[i].width < 1 || rects[i].height < 1)
continue;
return parseRect(rects[i]);
};
return null;
}
function parseRect(aRect) {
return {
top : aRect.top,
right : aRect.right,
bottom : aRect.bottom,
left : aRect.left,
width : aRect.width,
height : aRect.height,
centerX: aRect.left + (aRect.width / 2),
centerY: aRect.top + (aRect.height / 2),
};
}
function getAncestorLink(elem) {
while(elem = elem.parentNode) {
if (elem instanceof HTMLAnchorElement && elem.href ||
elem instanceof HTMLAreaElement && elem.href ||
elem instanceof HTMLLinkElement && elem.href)
{
return elem;
}
}
return null;
}
function $(id) { return document.getElementById(id); }
function $$(exp, doc) { return Array.prototype.slice.call((doc || document).querySelectorAll(exp)); }
// http://gist.github.com/321205
function $A(args) { return Array.prototype.slice.call(args); }
function log() { Application.console.log($A(arguments).join(', ')); }
function debug() { if (gCrossFireModoki.DEBUG) log($A(arguments).join(', ')) }
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment