There are 14 previous versions of this script.
// ==UserScript==
// @name HatebuComment on LDR
// @namespace http://d.hatena.ne.jp/ABCbo/
// @include http://reader.livedoor.com/reader/
// ==/UserScript==
// LastUpdate 2011.10.24
(function() {
// ショートカットキーの設定
const GET_COMMENT_KEY = 'm';
const TITLE_MODE_KEY = 'x';
const OPEN_BACKGROUND = 'n';
const OPEN_HATEBU_PAGE = 'h';
var w = unsafeWindow;
// 記事ID、コメント挿入ID、一覧表示前のスクロール位置、前クラス名・ID
var ID = ULID = POS = PREV = PREVID = '';
// JSONとRSSそれぞれのコメント取得が完了したかチェック
var jsonDone = rssDone = false;
var jsonArray = rssArray = [];
with(w) {
/* register_hookのタイミング{
// LDRを開いたとき(左から早い順)
BEFORE_INIT, AFTER_INIT, BEFORE_CONFIGLOAD, AFTER_CONFIGLOAD
BEFORE_SUBS_LOAD, AFTER_INIT_GUIDE, AFTER_SUBS_LOAD
// フィードを開くたび
BEFORE_PRINTFEED, AFTER_PRINTFEED
// ウィンドウサイズ変更時
WINDOW_RESIZE
// フィードリスト更新時
BEFORE_SUBS_LOAD, AFTER_SUBS_LOAD
// 設定画面(メニューの「その他」>「設定変更」)を開いたとき
AFTER_INIT_CONFIG
// 以下は不明
COMPLATE_PRINTFEED, AFTER_LOAD, BEFORE_UNLOAD, AFTER_INIT_MANAGE
BEFORE_ANYKEYDOWN, AFTER_ANYKEYDOWN }*/
// アクセス時一度だけの処理
register_hook('AFTER_INIT', function() {
// キーボードイベントの追加
// コメント取得(キーボード)
Keybind.add(GET_COMMENT_KEY, function() {
ID = get_active_item(true).id;
// タイトル一覧だった場合は、コメント表示⇔タイトル一覧で切り替える
if (document.body.className != '') {
PREV = document.body.className;
toggleTitle();
} else {
document.body.className = PREV ? PREV : '';
PREV = '';
}
getComment();
});
// タイトルのみを一覧表示する
Keybind.add(TITLE_MODE_KEY, function() {
ID = get_active_item(true).id;
PREV = '';
// 本文を表示状態に変更する
removeClass($("right_body"), "compact");
toggleTitle();
focusEntry();
});
// 背面タブで開いて次の記事に移動
Keybind.add(OPEN_BACKGROUND, function() {
var item = get_active_item(true);
if (!item) return;
// unsafeWindowからGM_~を利用するための処理
window.setTimeout(function() {
GM_openInTab(item.link);
}, 0);
Control.go_next();
});
// はてブページを開く
Keybind.add(OPEN_HATEBU_PAGE, function() {
var link = get_active_item(true).link.replace(/#/, "%23");
if (link) window.open("http://b.hatena.ne.jp/entry/" + link);
});
});
// フィードを開くたびに毎回(描画開始前)
register_hook('BEFORE_PRINTFEED', function(feed) {
// authorにはてブ数などを追加
feed.items.forEach(function(item) {
var link = item.link.replace(/#/g, '%23');
var author = item.author;
var id = item.id;
var base = 'http://b.hatena.ne.jp/entry/';
/* 2009.11.26あたりからauthorが空白でなくnullになるケースが出てきた
らしい。author == nullだとダメで === nullで判定できた */
if (author === null || !/hatebu_get_comment/.test(author)) {
var hatebu_image = ' '
+ '<span class="hatebu_link">'
+ '<span class="view_content' + id + '" '
+ 'title="この記事のコメントを見る">></span>'
+ '<a href="' + base + link + '">'
+ '<img style="border:none;" src="' + base + 'image/' + link + '">'
+ '</a></span>'
+ '<span class="' + id + ' hatebu_get_comment" '
+ 'title="コメント表示">▼表示</span>'
+ '<span id="container_' + id + '"></span>'
+ '<span class="' + id + ' hatebu_get_comment" '
+ 'title="コメント表示" style="display:none;">▼表示</span>'
item.author = author + hatebu_image;
}
});
});
// 描画開始後
register_hook('AFTER_PRINTFEED', function() {
/* ホイールスクロールで最後の記事までスクロール可能にする */
setStyle("scroll_padding",
{height: $("right_container").offsetHeight + "px"});
// マウスイベントの追加
var addEvent = setInterval(function() {
// 最後の記事まで表示されたのを確認してからイベントを追加する
var nodes = XPATH("//*[contains(concat(' ', @class, ' '), ' item ')]");
if (contain(nodes[nodes.length - 1].className, "last")) {
// コメント取得(マウス)
var nodes = XPATH('//span[contains(@class, "hatebu_get_comment")]');
nodes.forEach(function(node) {
node.addEventListener('click', function() {
ID = this.className.replace(/^(\d+?) .*$/, "$1");
PREV = '';
getComment();
}, false);
});
/* 「>」クリックで、その記事にフォーカスを当て本文も表示する */
var nodes = XPATH('//*[contains(@class, "view_content")]');
nodes.forEach(function(node) {
node.addEventListener('click', function(e) {
// 移動前のスクロール状態
POS = $('right_container').scrollTop;
// 移動先の記事ID
ID = this.className.replace(/view_content(.*)$/, "$1");
PREV = document.body.className;
toggleTitle();
// 本文を表示状態に変更する
removeClass($("right_body"), "compact");
focusEntry();
// ↓本文表示と同時にコメント取得も行う場合はコメントを外す
//getComment();
// クリックした位置にタイトル一覧に戻す「>」を表示する
$('onlyTitle').style.cssText += 'left: ' + (e.clientX - 5) + 'px;'
+ 'top: ' + (e.clientY - 7) + 'px;';
$('onlyTitle').style.display = 'block';
}, false);
});
clearInterval(addEvent);
}
}, 200);
// 次のフィードに移動したら「>」を隠す
$('onlyTitle').style.display = 'none';
});
// 関数のまとめ
// タイトルのみ表示を切り替える
function toggleTitle() {
PREVID = '';
if (document.body.className != 'only_title') {
document.body.className = 'only_title';
$('onlyTitle').style.display = 'none';
} else {
document.body.className = '';
}
}
//矢印切り替え
function toggleArrow(mode) {
var nodes = XPATH(
"//span[contains(concat(' ', @class, ' '), ' " + ID + " ')]");
for (var i = 0, node; node = nodes[i]; i++) {
if (mode == 'show') {
node.innerHTML = '▲隠す';
node.title = 'コメントを隠す';
node.style.display = 'inline';
} else {
node.innerHTML = '▼表示';
node.title = 'コメント表示';
if (i == 1) node.style.display = 'none';
}
}
}
// コメント切り替え
function toggleComment() {
focusEntry();
var elem = $(ULID);
if (PREV) {
elem.style.display = '';
} else if (elem.style.display != 'none') {
elem.style.display = 'none';
toggleArrow('hide');
} else {
elem.style.display = '';
toggleArrow('show');
}
}
// スクロールして記事にフォーカス
function focusEntry(old) {
if (old) {
Control.scroll_to_px(POS + 2);
return;
}
var elem = $('container_' + ID);
Control.scroll_to_px(
elem.parentNode.parentNode.parentNode.offsetTop + 2);
}
// コメント挿入用の要素を作成
function beginAppend() {
var elem = $('container_' + ID);
var ul = document.createElement('ul');
ul.className = 'hatebu';
ul.id = ULID;
elem.appendChild(ul);
}
// コメント取得の前処理
function getComment() {
if (PREVID == '') PREVID = ID;
if (PREVID != ID) {
document.body.className = '';
PREV = '';
}
PREVID = ID;
var item = get_item_info(ID);
ULID = 'hatebu_' + ID;
if ($(ULID)) {
toggleComment();
} else {
jsonDone = rssDone = false;
beginAppend();
focusEntry();
window.setTimeout(function() {
getJSON(item.link);
getRSS(item.link);
message('コメント読み込み中...');
}, 0);
var timer = setInterval(function() {
if (jsonDone && rssDone) {
message('読み込み完了');
var li = document.createElement('li');
li.className = 'line_one';
li.innerHTML = 'コメントのあるはてブ数 : ';
if (jsonArray.length >= rssArray.length) {
li.innerHTML += jsonArray.length + '(json) >= '
+ rssArray.length + '(rss) なのでjsonの結果を表示';
var ARRAY = jsonArray;
} else {
li.innerHTML += rssArray.length + '(rss) >= '
+ jsonArray.length + '(json) なのでrssの結果を表示';
var ARRAY = rssArray;
}
var place = $(ULID);
place.appendChild(li);
if (ARRAY.length == 0) {
noComment();
} else {
for (var i in ARRAY) {
var li = document.createElement('li');
li.innerHTML = ARRAY[i];
place.appendChild(li);
}
}
toggleArrow('show');
clearInterval(timer);
}
}, 200);
}
}
function XPATH(expression) {
var nodes = document.evaluate(expression, document, null, 4, null);
var array = [];
while (node = nodes.iterateNext()) {
array.push(node);
}
if (array.length == 0) {
GM_log("xpath error : " + expression);
} else {
return array;
}
}
// コメントがない場合
function noComment() {
var li = document.createElement('li');
li.innerHTML = "No Comment.";
$(ULID).appendChild(li);
}
// 要素の追加
// ?で表示されるショートカットキー一覧に設定したキーを追加する
var tr = document.createElement('table');
tr.innerHTML = "<tr><th>コメント取得 / 切り替え:</th>"
+ "<td><kbd>" + GET_COMMENT_KEY + "</kbd></td>"
+ "<th>タイトルモードの切り替え:</th>"
+ "<td><kbd>" + TITLE_MODE_KEY + "</kbd></td>"
+ "<th>背面タブで開く:</th>"
+ "<td><kbd>" + OPEN_BACKGROUND + "</kbd></td>"
+ "<th>はてブページを開く:</th>"
+ "<td><kbd>" + OPEN_HATEBU_PAGE + "</kbd></td>";
$("keyhelp").insertBefore(tr, $("keybind_table"));
// スタイルの設定
var style = document.createElement('style');
style.innerHTML = '.hatebu {'
+ 'border-top:1px dashed LightGray; border-bottom:1px dashed '
+ 'LightGray; list-style-type:circle; background-color:#EDF1FD;}'
+ '.hatebu li {margin-bottom: 3px; border-top:1px dotted LightGray;}'
+ '.line_one {border:none !important;}'
+ '.hatebu span.tags a {font-size: 85%; color:RoyalBlue; '
+ 'text-decoration:none;}'
+ '.date {font-size: 85%; color:Gray;}'
+ '.hatebu_get_comment {cursor: pointer !important; font-size: 13px;}'
+ '.hatebu_count {font-size: 13px;}'
// タイトルのみを表示するためのスタイル
+ '.only_title .item_footer, .only_title .item_body {display: none;}'
+ '.only_title .item_buttons {visibility: hidden;}'
+ '.only_title .item_buttons *[id^="pin_"] {'
+ 'visibility: visible; position: absolute; right: 0px;}'
+ '.only_title .item_title {height: 20px !important; overflow: hidden; '
+ 'padding: 0 !important; position: relative; left: 60px; '
+ 'margin-right: 70px !important;}'
+ '.only_title .item_info {visibility: hidden; height: 0px; '
+ 'padding: 0 !important;}'
+ '.only_title .hatebu_link {position: absolute; left: 0px; '
+ 'visibility: visible;}'
+ '.only_title .hatebu_link span {position: relative; top: -18px;}'
+ '.only_title .hatebu_link img {position: relative; top: -15px;}'
+ '.hatebu_link > span {visibility: hidden;}'
+ '.only_title .hatebu_link > span {visibility: visible;}'
+ '.only_title #onlyTitle {display: none;}'
+ '#message_box {margin-left: 70px;}';
document.body.appendChild(style);
// マウスで本文の表示を切り替える
var elem = document.createElement('li');
elem.id = 'toggleContent';
elem.title = '本文の表示 / 非表示の切替';
elem.innerHTML = '本文';
$("control_buttons_ul").appendChild(elem);
$("toggleContent").addEventListener('click', function() {
// LDRの関数
Control.compact()
}, false);
// マウスでタイトル一覧表示を切り替える
var elem = document.createElement('li');
elem.id = 'toggleTitle';
elem.title = 'タイトルのみ表示の切替';
elem.innerHTML = 'タイトル';
$("control_buttons_ul").appendChild(elem);
$("toggleTitle").addEventListener('click', function() {
ID = get_active_item(true).id;
toggleTitle();
focusEntry();
}, false);
// タイトル一覧表示に戻す「>」
var elem = document.createElement('span');
elem.id = 'onlyTitle';
elem.title = 'タイトル一覧に戻る';
elem.innerHTML = '>';
elem.style.position = 'absolute';
document.body.appendChild(elem);
$("onlyTitle").addEventListener('click', function() {
toggleTitle();
focusEntry(true);
}, false);
}//with(w)
// unsafeWindow内ではGM_xmlhttpRequestは使えない
// JSONでコメント取得
function getJSON(link) {
// 関連エントリのないjsonliteの方が軽い
GM_xmlhttpRequest({
method:"GET",
url: 'http://b.hatena.ne.jp/entry/jsonlite/?url='
+ encodeURIComponent(link),
onload:function(xhr) {
var json = eval("(" + xhr.responseText + ")");
//レスポンスの処理
jsonArray = [];
if (!json) return jsonDone = true;
json.bookmarks.forEach(function(user) {
var name = user.user;
if (user.comment.length > 0) {
var date = getDate(user.timestamp);
var Date = date.replace(/^(....)(..)(..)/, "($1/$2/$3)");
jsonArray.push('<span class="date">' + Date + '</span>'
+ ' <img src="http://www.hatena.ne.jp/users/'
+ name.substring(0,2)
+ '/' + name + '/profile_s.gif">'
+ ' <a href="http://b.hatena.ne.jp/' + name + '/' + date
+ '#bookmark-' + json.eid + '">' + name + '</a>'
+ ' <span class="tags">'
+ user.tags.map(function(x){
return '<a href="http://b.hatena.ne.jp/' + name + '/'
+ x + '/">' + x + '</a>'
}).join(', ')
+ '</span> ' + user.comment);
}
});
jsonDone = true;
}
});
}
// jsonでなぜか結果がほとんど帰ってこないケースがあるので
// RSSでも取得できるようにする
function getRSS(link, place) {
GM_xmlhttpRequest({
method:"GET",
url: "http://b.hatena.ne.jp/entry/rss/" + link,
onload:function(res) {
rssArray = [];
var r = res.responseText.match(/<item rdf:about(\s|.)+?<\/item>/mg);
if (!r) {
return rssDone = true;
}
for (var i = 0, j = 0; i < r.length; i++) {
var str = '';
var tags = r[i].match(/<dc:subject>(.*?)<\/dc:subject>/g);
r[i].match(/<title>(.*?)<\/title>(?:\s|.)+?<link>(.*?)<\/link>(?:\s|.)+?<description>(.*?)<\/description>(?:\s|.)+?<dc:date>(....)-(..)-(..).(..:..:..)/);
var name = RegExp.$1;
var link = RegExp.$2;
var comment = RegExp.$3;
if (comment.length > 0) {
var Date = '(' + RegExp.$4 + '/'
+ RegExp.$5 + '/' + RegExp.$6 + ')';
str = '<span class="date">' + Date + '</span>'
+ ' <img src="http://www.hatena.ne.jp/users/'
+ name.substring(0,2) + '/' + name + '/profile_s.gif">'
+ ' <a href="' + link+ '">' + name + '</a>'
+ ' <span class="tags">';
if (tags) {
var tmp = new Array();
for (var n in tags) {
var tag = tags[n].replace(/<\/?dc:subject>/g, "");
tmp.push('<a href="http://b.hatena.ne.jp/' + name + '/'
+ tag + '/">' + tag + '</a>');
}
str += tmp.join(', ');
}
str += '</span>' + comment;
rssArray.push(str);
}
}
rssDone = true;
}
});
}
function getDate(timestamp) {
var date = new Date(timestamp);
var y = date.getFullYear();
var m = date.getMonth() + 1;
var d = date.getDate();
if (m < 10) m = "0" + m;
if (d < 10) d = "0" + d;
return y.toString() + m.toString() + d.toString();
}
})();