Created
July 9, 2010 09:24
-
-
Save Griever/469269 to your computer and use it in GitHub Desktop.
Firefox で空間ナビゲーションを再現する uc.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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