launchAutoPager 関数で引数を調べ、現在ページに該当する list(SITEINFO) があれば引数にして AutoPager を new する(570行目付近)
var launchAutoPager = function(list) {
if (list.length == 0) {
return
}
for (var i = 0; i < list.length; i++) {
try {
if (ap) {
return
}
else if (!location.href.match(list[i].url)) {
}
else if (!getFirstElementByXPath(list[i].nextLink)) {
FIXME
if (list[i].url.length > 12 ) {
debug("nextLink not found.", list[i].nextLink)
}
}
else if (!getFirstElementByXPath(list[i].pageElement)) {
if (list[i].url.length > 12 ) {
debug("pageElement not found.", list[i].pageElement)
}
}
else {
ap = new AutoPager(list[i])
return
}
}
catch(e) {
log(e)
continue
}
}
}
- 正常系シナリオ1は、スクリプト冒頭で定数定義された SITEINFO 配列を引数に launchAutoPager を呼んでいました。その SITEINFO 配列はデフォルトでコメントアウトされているので長さゼロ。launchAutoPager 関数は最初に引数の配列の長さを調べており(list.length)、長さゼロなら return する。つまり、デフォルトでシナリオ1は何もしない。自分で SITEINFO を書き加えて初めてシナリオ1は機能する
- for ループ:配列の要素数(list.length)と同じ回数ループする
- if(ap):ap は前回、初期化の最後で null クリアしていたので if の中には入らずスルー
- 最初の else if:引数の配列の各要素(=連想配列)について、そのキー url に対応する値を調べ、現在ブラウザで閲覧中の URL とマッチ(match)しなければループ先頭に戻って次の要素に進み(i++)ループ内を最初から繰り返す
- 2番目の else if:キー nextLink の値を getFirstElementByXPath(↓直後に解説あり↓) で取得している。閲覧中のページ内に一致するノードがなければ null が帰ってきて、(定数定義で DEBUG = true に設定していたなら、コンソールに"nextLink not found.<見つからなかったXPath>"を出力し)ループ先頭に戻って次の要素に進み(i++)ループ内を最初から繰り返す
- 3番目の else if:キー pageElement の値をgetFirstElementByXPath(↓直後に解説あり↓) で取得している。閲覧中のページ内に一致するノードがなければ null が帰ってきて、(定数定義で DEBUG = true なら、コンソールに"pageElement not found.<見つからなかったXPath>"を出力し)ループ先頭に戻って次の要素に進み(i++)ループ内を最初から繰り返す
- else:この条件に該当するなら有効な SITEINFO が見つかったと判定し、該当した list(SITEINFO) を引数に AutoPager オブジェクトを new して return する
- 例外が発生した場合、例外オブジェクトをログ出力しループ処理を継続
- 続きは70行目付近の AutoPager の定義(utility functions を解説した後に続く)
getFirstElementByXPath関数で XPath に該当する最初の要素を取得(utility functions)
function getFirstElementByXPath(xpath, node) {
var result = getXPathResult(xpath, node,
XPathResult.FIRST_ORDERED_NODE_TYPE)
return result.singleNodeValue
}
- getFirstElementByXPath の定義はスクリプト後方の utility functions にある。見に行くと...
- 引数2つ!?...いやいや慌てることはない。足りない引数は undefined となるだけだ
- 同じく utility functions の ↓ getXPathResult ↓ 関数を第3引数に XPathResult.FIRST_ORDERED_NODE_TYPE に設定して呼び出している。そうすると、一致したノード集合の最初の要素だけを取ってくる
- singleNodeValue で中身を取り出して return
getXPathResult 関数で XPath で指定したノードを取得(utility functions)
function getXPathResult(xpath, node, resultType) {
var node = node || document
var doc = node.ownerDocument || node
var resolver = doc.createNSResolver(node.documentElement || node)
var defaultNS = node.lookupNamespaceURI(null)
if (defaultNS) {
const defaultPrefix = '__default__'
xpath = addDefaultPrefix(xpath, defaultPrefix)
var defaultResolver = resolver
resolver = function (prefix) {
return (prefix == defaultPrefix)
? defaultNS : defaultResolver.lookupNamespaceURI(prefix)
}
}
return doc.evaluate(xpath, node, resolver, resultType, null)
}
- if(defaultNS):もし閲覧中のページでデフォルト名前空間があった場合...(えぇいめんどくさい)は自習とします(要望があれば書く)
- 最後に evaluate で XPath に対応するノードを return しています
AutoPager(70行目付近)により周辺機能の初期化及びページ継ぎ足しのトリガー・イベントの登録を行う
{
"pageElement": "id(\"days\")/div",
"url": "^https?://(?:d|[^.]+\\.g)\\.hatena\\.ne\\.jp/",
"nextLink": "//a[@rel=\"prev\"]",
"exampleUrl": "http://os0x.g.hatena.ne.jp/os0x/"
}
var AutoPager = function(info) {
this.pageNum = 1
this.info = info
this.state = AUTO_START ? 'enable' : 'disable'
var self = this
var url = this.getNextURL(info.nextLink, document, location.href)
if ( !url ) {
debug("getNextURL returns null.", info.nextLink)
return
}
if (info.insertBefore) {
this.insertPoint = getFirstElementByXPath(info.insertBefore)
}
if (!this.insertPoint) {
var lastPageElement = getElementsByXPath(info.pageElement).pop()
if (lastPageElement) {
this.insertPoint = lastPageElement.nextSibling ||
lastPageElement.parentNode.appendChild(document.createTextNode(' '))
}
}
if (!this.insertPoint) {
debug("insertPoint not found.", lastPageElement, info.pageElement)
return
}
this.requestURL = url
this.loadedURLs = {}
this.loadedURLs[location.href] = true
var toggle = function() {self.stateToggle()}
this.toggle = toggle
GM_registerMenuCommand('AutoPagerize - on/off', toggle)
this.scroll= function() { self.onScroll() }
window.addEventListener("scroll", this.scroll, false)
this.initIcon()
this.initHelp()
this.icon.addEventListener("mouseover",
function(){self.viewHelp()}, true)
var scrollHeight = getScrollHeight()
var bottom = getElementPosition(this.insertPoint).top ||
this.getPageElementsBottom() ||
(Math.round(scrollHeight * 0.8))
this.remainHeight = scrollHeight - bottom + BASE_REMAIN_HEIGHT
this.onScroll()
}
- 長いので、分割ブラウザを使うなり工夫しましょう...俺が...
- 関数の頭で初期化がいくつかあり、↓ getNextURL ↓ 関数では次ページを指す URL を絶対パスで取得している
- url(次ページの URL)が取得できなかったら return して終了
- info.insertBefore つまり SITEINFO で insertBefore が指定されていれば、該当する最初のノードを取得し insertPoint プロパティに代入
- insertPoint プロパティがなければ(つまり SITEINFO の insertBefore が設定されていない、もしくは該当ノードがなければ) ↓ getElementsByXPath ↓ 呼び出し、結果のノード集合を配列をすぐに pop() して配列最後のノードを取り出し lastPageElement に代入
- insertPoint プロパティがなければ return して終了
- 次ページの URL を requestURL プロパティに設定
- 初期化した連想配列 loadedURLs に 現在ページの URL をキーに true を設定
- toggle メソッド呼び出しを ↓ stateToggle ↓ メソッドに転送するように設定
- Greasemonkey のメニューに 'AutoPagerize - on/off' として toggle メソッド呼び出しを登録
- scroll メソッドを onScroll メソッドに転送するように設定
- window.addEventListener でスクロール時に呼び出される onScroll メソッドを登録
- ↓ initIcon ↓ メソッド呼び出しで右上のアイコンを設定
- ↓ initHelp ↓ メソッド呼び出しで右上のヘルプを設定(マウス・オーバーするまで見えませんが...)
- icon つまり画面右上にある四角形の div に addEventListener で mouseover イベントを追加。↓ viewHelp ↓ メソッドが呼ばれるよう設定しています(3番目の引数 useCapture が true の理由は未確認。フォロー求む)
- ↓ getScrollHeight ↓ でまだ見えていない部分も含めたドキュメント全体の高さを求め scrollHeight に代入
- ↓ getElementPosition ↓ 関数で次ページを挿入する位置をドキュメント上部からの差異(offset) の総計(単位ピクセル)として求める、失敗したら getPageElementsBottom メソッドで SITEINFO の pageElement の最後尾の下端の位置を求める、それも失敗したら先ほど求めた scrollHeight の 80% を整数に丸めた値を bottom に代入
- scrollHeight(見えない部分も含めたドキュメント全体の高さ)- bottom(ページ継ぎ足し開始位置までの距離)+ BASE_REMAIN_HEIGHT(定数定義で指定。ページ継ぎ足し位置よりどのくらい上でページ継ぎ足しを開始するか)を remainHeight (ページ継ぎ足しを開始する残りの高さ)プロパティに代入
getNextURL で「次のページ」のURLを取得
AutoPager.prototype.getNextURL = function(xpath, doc, url) {
var nextLink = getFirstElementByXPath(xpath, doc)
if (nextLink) {
var nextValue = nextLink.href || nextLink.action || nextLink.value
if (nextValue.match(/^http(s)?:/)) {
return nextValue
}
else {
var base = getFirstElementByXPath('//base[@href]', doc)
return resolvePath(nextValue, (base ? base.href : url))
}
}
}
引数 |
渡ってくる引数の中身 |
xpath |
info.nextLink なので、次のページへのアンカータグ(<a href="...url文字列...">)を指すXPath |
doc |
document |
url |
location.href なので、閲覧中ページの URL |
- 前も使った utility functions の getFirstElementByXPath で次ページへのアンカー(=リンク)を取得
- nextLink が無事取得できたら
- アンカーの属性値 href なければ action なければ value を nextValue に代入する。nextValue は次ページの URL
- もし nextValue が http: もしくは https: で始まる絶対パスなら nextValue をそのまま return
- それ以外(相対指定など)の場合、href 属性を持つ base エレメントを取得し、utility functions の ↓ resolvePath ↓ 関数の戻り値、つまり絶対パス化された URL を return
resolvePath 関数で相対パスを絶対パスに変換(utility functions)
function resolvePath(path, base) {
var XHTML_NS = "http://www.w3.org/1999/xhtml"
var XML_NS = "http://www.w3.org/XML/1998/namespace"
var a = document.createElementNS(XHTML_NS, 'a')
a.setAttributeNS(XML_NS, 'xml:base', base)
a.href = path
return a.href
}
引数 |
渡ってくる引数の中身 |
path |
次ページへのアンカーに設定されている、絶対パス指定以外の URL |
base |
base エレメントが取得できていれば、その href 属性値。取得できなければ閲覧中ページの URL |
- createElementNS 関数で名前空間付きのアンカーを作成
- 作成したアンカーに setAttributeNS 関数で属性値 base を設定
- 作成したアンカーの href 属性に引数 path を代入
- アンカーの href を return
getElementsByXPath 関数で XPath で指定されたノード(集合)を取得(utility functions)
function getElementsByXPath(xpath, node) {
var nodesSnapshot = getXPathResult(xpath, node, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
var data = []
for (var i = 0; i < nodesSnapshot.snapshotLength; i++) {
data.push(nodesSnapshot.snapshotItem(i))
}
return data
}
引数 |
渡ってくる引数の中身 |
xpath |
SITEINFO の pageElement |
node |
呼び出し時に指定されていなかったので undefined |
stateToggle メソッドで画面右上の ON/OFF 表示を切り替える
AutoPager.prototype.stateToggle = function() {
if (this.state == 'enable') {
this.disable()
}
else {
this.enable()
}
}
- state プロパティが 'enable' なら disable メソッド呼び出し
- それ以外なら enable メソッド呼び出し
enable メソッドで右上のアイコン表示を 'on' へ切り替え
AutoPager.prototype.enable = function() {
this.state = 'enable'
this.icon.style.background = COLOR['on']
this.icon.style.opacity = 1
}
- 状態フラグを 'enable' に
- 右上の四角いアイコンの色を定数定義で COLOR 連想配列のキー 'on' の値に設定
- アイコンを不透明に設定
disable メソッドで右上のアイコン表示を 'off' へ切り替え
AutoPager.prototype.disable = function() {
this.state = 'disable'
this.icon.style.background = COLOR['off']
this.icon.style.opacity = 0.5
}
- 状態フラグを 'disable' に
- 右上の四角いアイコンの色を定数定義で COLOR 連想配列のキー 'off' の値に設定
- アイコンを透過度 50% を設定
initIcon で右上のアイコンを設定
AutoPager.prototype.initIcon = function() {
var div = document.createElement("div")
div.setAttribute('id', 'autopagerize_icon')
with (div.style) {
fontSize = '12px'
position = 'fixed'
top = '3px'
right = '3px'
background = COLOR['on']
color = '#fff'
width = '10px'
height = '10px'
zIndex = '255'
if (this.state != 'enable') {
background = COLOR['off']
}
}
document.body.appendChild(div)
this.icon = div
}
- div エレメントを作成し、id 属性に 'autopagerize_icon' と設定。スタイルを設定した後で body エレメントに appendChild。最後に div を this.icon に代入しとく
- これが、右上の四角形の正体
initHelp で右上のアイコンをホバーさせたときに出てくるヘルプメニュー(?)を設定
AutoPager.prototype.initHelp = function() {
var helpDiv = document.createElement('div')
helpDiv.setAttribute('id', 'autopagerize_help')
helpDiv.setAttribute('style', 'padding:5px;position:fixed;' +
'top:-200px;right:3px;font-size:10px;' +
'background:#fff;color:#000;border:1px solid #ccc;' +
'z-index:256;text-align:left;font-weight:normal;' +
'line-height:120%;font-family:verdana;')
var toggleDiv = document.createElement('div')
toggleDiv.setAttribute('style', 'margin:0 0 0 50px;')
var a = document.createElement('a')
a.setAttribute('class', 'autopagerize_link')
a.innerHTML = 'on/off'
a.href = 'javascript:void(0)'
var self = this
var toggle = function() {
self.stateToggle()
helpDiv.style.top = '-200px'
}
a.addEventListener('click', toggle, false)
toggleDiv.appendChild(a)
var s = '<div style="width:100px; float:left;">'
for (var i in COLOR) {
s += '<div style="float:left;width:1em;height:1em;' +
'margin:0 3px;background-color:' + COLOR[i] + ';' +
'"></div><div style="margin:0 3px">' + i + '</div>'
}
s += '</div>'
var colorDiv = document.createElement('div')
colorDiv.innerHTML = s
helpDiv.appendChild(colorDiv)
helpDiv.appendChild(toggleDiv)
var versionDiv = document.createElement('div')
versionDiv.setAttribute('style', 'clear:both;')
versionDiv.innerHTML = '<a href="' + URL +
'">AutoPagerize</a> ver ' + VERSION
helpDiv.appendChild(versionDiv)
document.body.appendChild(helpDiv)
var proc = function(e) {
var c_style = document.defaultView.getComputedStyle(helpDiv, '')
var s = ['top', 'left', 'height', 'width'].map(function(i) {
return parseInt(c_style.getPropertyValue(i)) })
if (e.clientX < s[1] || e.clientX > (s[1] + s[3] + 11) ||
e.clientY < s[0] || e.clientY > (s[0] + s[2] + 11)) {
helpDiv.style.top = '-200px'
}
}
helpDiv.addEventListener('mouseout', proc, false)
this.helpLayer = helpDiv
GM_addStyle('#autopagerize_help a { color: #0f0; text-decoration: underline;}')
}
- div エレメント作成し helpDiv に代入。id 属性 'autopagerize_help' と style 属性を設定(top:-200px で見えなくしている。この div は右上の四角形でマウスをホバーすると表示されるやつ)
- div エレメント作成し toggleDiv に代入し、style 属性で左にマージンを設定
- アンカー(a エレメント)を作成し、class 属性の 'autopagerize_link' やテキストノード 'on/off'、href 属性を設定
- toggle メソッドを定義。アイコンの変化に加え helpDiv の非表示(top:-200px)が追加されている
- a.addEventListener でアンカーがクリックされたとき呼び出される toggle メソッドを登録
- toggleDiv にアンカーを appendChild(toggleDiv の子供ノードにアンカー が追加された)
- div エレメントを作成し colorDiv に代入。innerHTML で直前に 変数 s や for ループで作成した文字列を流し込んでいる。これは右上の四角形でマウスでホバーさせると表示される左上の部分
- colorDiv、toggleDiv を helpDiv に appendChild
- div エレメントを作成し versionDiv に代入。回り込み回避の clear:both スタイル、アンカー文字列を設定
- versionDiv を helpDiv に appendChild
- helpDiv を body に appendChild
式 |
説明 |
e.clientX |
イベント発生場所(マウスポインタ)の水平位置(始点は左) |
e.clientY |
イベント発生場所(マウスポインタ)の垂直位置(始点は上) |
s[0] |
helpDiv の上端の位置 |
s[1] |
helpDiv の左端の位置 |
s[2] |
helpDIv の高さ |
s[3] |
helpDiv の幅 |
11 |
padding * 2 + border = 5 * 2 + 1 |
e.clientX < s[1] |
イベントの発生場所(マウスポインタの水平位置)が helpDiv の左端より左にある |
e.clientX > (s[1] + s[3] + 11) |
イベントの発生場所(マウスポインタの水平位置)が helpDiv の右端より右にある |
e.clientY < s[0] |
イベントの発生場所(マウスポインタの垂直位置)が helpDiv の上端より上にある |
e.clientY > (s[0] + s[2] + 11) |
イベントの発生場所(マウスポインタの垂直位置)が helpDiv の下端より下にある |