HatenaBookmarkAdvancedSuggestion-20061114.user.js
今日の朝のエントリ(id:nozom:20061113:1163372094)は自分としては大したことのない修正だったのに、 もブックマークされていてちょっと驚いた。それで、なんだか申し訳ないので、はてなブックマークのタグ補完を改善: タグとコメントを別々に入力できるようにするのスクリプトもバージョンアップした。
HatenaBookmarkAdvancedSuggestion-20061114.user.js
主な変更点
- タブキーで補完するとき、複数候補があったら共通する部分だけを補完するようにした
- ブックマーク済みのページを開いたときに実行される、コメントからタグへの展開のバグを修正した
// ==UserScript== // @name Yet Another Hatena Bookmark Tag Suggestion // @namespace http://d.hatena.ne.jp/nozom/ // @description Improve Tag Suggestion of Hatena Bookmark // @include http://b.hatena.ne.jp/add?* // @include http://b.hatena.ne.jp/*/edit?* // ==/UserScript== // This script is based on tag_suggest.js. (function() { const KEY_BS = 8; const KEY_TAB = 9; const KEY_ENTER = 13; const KEY_SHIFT = 16; const KEY_ESC = 27; const KEY_LEFT = 37; const KEY_UP = 38; const KEY_RIGHT = 39; const KEY_DOWN = 40; var candidatesListDiv; var userTagsListDiv; var otherTagsListDiv; var keywordsListDiv; var commentInput; var tagInput; var addedTags = new Array(); var candidates = new Array(); var suggesting = false; var selectedIndex = 0; var wordStartPos = 0; function moveCaret(target, index) { target.setSelectionRange(index, index); } function getCaretPos(target) { return target.selectionStart; } function getWordStartPos(target) { var str = target.value; var pre = str.substring(0, getCaretPos(target)); if (pre.match(/^(.*, *)/)) { return RegExp.$1.length; } else { return 0; } } function getNormalizedTags(target) { return unsafeWindow.getNormalizedTags(target); } function getLowerTags(target) { return unsafeWindow.getLowerTags(target); } function getAbsoluteLeft(element) { return unsafeWindow.getAbsoluteLeft(element); } function getAbsoluteTop(element) { return unsafeWindow.getAbsoluteTop(element); } function updateAddedTags() { var tmpTags = tagInput.value.split(/, */); tmpTags.pop(); newTags = new Array(); for (var i = 0; i < tmpTags.length; i++) { var tag = tmpTags[i].replace(/, */, ""); if (tag != "") { newTags.push(tag); } } if (newTags.length != addedTags.length) { addedTags = newTags; return true; } return false; } function isAdded(tagName) { for (var i = 0; i < addedTags.length; i++) { if (addedTags[i] == tagName) return true; } return false; } function addTag(tagName) { tagInput.value = tagInput.value.replace(/(, *)$/, ""); if (tagInput.value != "") tagInput.value += ", "; tagInput.value += tagName + ", "; } function removeTag(tagName) { var s = tagInput.value + ", "; tagInput.value = s.replace(/([^ ,]?[^,]*), */g, function(t) { t = RegExp.$1; return (t == "") || (t == tagName) ? "" : t + ", "; }); } function updateCommentTags() { var s = ""; for (var i = 0; i < addedTags.length; i++) { s += "[" + addedTags[i] + "]"; } with (commentInput) { value = value.replace(/^((\[[^\[\]]+?\])*)/, s); } } // // 補完一覧 // function createCandidateWord(tagName, selected) { var tagDiv = document.createElement("div"); with (tagDiv.style) { padding = "1 5 1 5"; fontSize = "10pt"; } if (selected) { tagDiv.style.backgroundColor = "#6666FF"; tagDiv.style.color = "white"; } tagDiv.appendChild(document.createTextNode(tagName)); return tagDiv; } function removeCandidatesList() { var oldCandidatesListDiv = candidatesListDiv.firstChild; if (oldCandidatesListDiv) oldCandidatesListDiv.parentNode.removeChild(oldCandidatesListDiv); } function updateCandidatesList() { if (! suggesting || candidates.length==0) removeCandidatesList(); else { var div = document.createElement("div"); with (div.style) { marginTop = "3"; backgroundColor = "white"; fontFamily = "sans-serif"; position = "absolute"; left = getAbsoluteLeft(tagInput); top = getAbsoluteTop(tagInput) + tagInput.clientHeight; width = tagInput.clientWidth; } for (var i = 0; i < candidates.length; i++) { var selected = (i == selectedIndex); div.appendChild(createCandidateWord(candidates[i], selected)); } var tab = document.createElement("div"); with (tab.style) { padding = "1 0 1 5"; marginTop = "3"; fontSize = "8pt"; borderTop = "1px solid #999999"; color = "#999999"; } tab.appendChild(document.createTextNode("決定[Enter][Tab]")); if (candidates.length >= 2) { tab.appendChild(document.createTextNode(" / 選択[↑][↓]")); } div.appendChild(tab); removeCandidatesList(); candidatesListDiv.appendChild(div); } } // // ユーザーのタグ一覧 // function selectTag(tag) { if (tag.wrappedJSObject) tag = tag.wrappedJSObject; tag.style.backgroundColor = "#dddddd"; tag.style.color = "black"; tag.onmouseover = function() {}; tag.onmouseout = function() {}; tag.onmousedown = function() { removeTag(this.firstChild.nodeValue); updateAllTagsLists(); updateCommentTags(); }; tag.onmouseup = function() { tagInput.focus(); moveCaret(tagInput, tagInput.value.length); }; } function unselectTag(tag) { if (tag.wrappedJSObject) tag = tag.wrappedJSObject; tag.style.backgroundColor = "white"; tag.style.color = "#777777"; tag.onmouseover = function() { this.style.backgroundColor = "#eeeeee"; }; tag.onmouseout = function() { this.style.backgroundColor = "white"; }; tag.onmousedown = function() { addTag(this.firstChild.nodeValue); updateAllTagsLists(); updateCommentTags(); }; tag.onmouseup = function() { tagInput.focus(); moveCaret(tagInput, tagInput.value.length); }; } function appendUserTagsList() { if (tags.length == 0) return; // 選択・非選択 for (var i = 0; i < tags.length; i++) { if (isAdded(tags[i])) { var tag = document.getElementById("tag" + i); selectTag(tag); } else { var tag = document.getElementById("tag" + i); unselectTag(tag); } } } // // 他人のタグ一覧 // function appendOtherTagsList() { if (otherTags.length==0) return; // 選択・非選択 for (var i = 0; i < otherTags.length; i++) { if (isAdded(otherTags[i])) { selectTag(document.getElementById("otherTag" + i)); } else { unselectTag(document.getElementById("otherTag" + i)); } } } // // キーワード一覧 // function appendKeywordsList() { if (keywords.length==0) return; // 選択・非選択 for (var i = 0; i < keywords.length; i++) { if (isAdded(keywords[i])) { selectTag(document.getElementById("keyword" + i)); } else { unselectTag(document.getElementById("keyword" + i)); } } } // // ハンドラ // function preventDefaultEvent(e) { e.preventDefault(); e.stopPropagation(); } function getCommonString() { var word = candidates[selectedIndex]; for (var common_length = 0; common_length < word.length; common_length++) { var common_string = word.substring(0, common_length + 1).toLowerCase(); for (var i = 0; i < candidates.length; i++) { if (candidates[i].substring(0, common_length + 1).toLowerCase() != common_string) return word.substring(0, common_length); } } return word; } function insertCompletion(selectOne) { if (suggesting) { // 文字挿入 var word; if (selectOne || (candidates.length == 1)) { word = candidates[selectedIndex] + ", "; } else { word = getCommonString(); } var currentPos = getCaretPos(tagInput); var str = tagInput.value; var pre = str.substring(0, getWordStartPos(tagInput)); var suf = str.substring(currentPos); tagInput.value = pre + word + suf; // 描画更新 updateCandidatesList(); updateAllTagsLists(); updateCommentTags(); // フォーカス var pos = getWordStartPos(tagInput) + word.length; moveCaret(tagInput, pos); tagInput.focus(); } else { var value = tagInput.value; if (tagInput.value.match(/(, *)$/)) { var len = RegExp.$1.length; if (len == 2) return false; value = value.substring(0, value.length - len); } value += ", "; tagInput.value = value; // 描画更新 // updateCandidatesList(); updateAllTagsLists(); updateCommentTags(); // フォーカス var pos = tagInput.value.length; moveCaret(tagInput, pos); tagInput.focus(); } return true; } function onKeyPressHandler(e) { var ascii = (e.charCode) ? e.charCode : ((e.which) ? e.which : e.keyCode); switch (ascii) { case KEY_TAB: if ((! candidates) || (candidates.length == 0)) { suggesting = false; selectedIndex = 0; } if (suggesting) { insertCompletion(false); } else { if (! insertCompletion(true)) { return; } } preventDefaultEvent(e); break; case KEY_ENTER: if ((! candidates) || (candidates.length == 0)) { suggesting = false; selectedIndex = 0; } if (suggesting) { insertCompletion(true); suggesting = false; } else { if (! insertCompletion(true)) { moveCaret(commentInput, commentInput.value.length); commentInput.focus(); } } preventDefaultEvent(e); break; } switch (e.keyCode) { case KEY_TAB: case KEY_UP: case KEY_DOWN: // case KEY_LEFT: // case KEY_RIGHT: // case KEY_ENTER: if (suggesting) { preventDefaultEvent(e); // for Gecko } break; } } function onKeyDownHandler(e) { switch (e.keyCode) { case KEY_UP: case KEY_DOWN: if (! suggesting) break; if (candidates.length == 0) { suggesting = false; selectedIndex = 0; break; } // 候補選択 if (e.keyCode == KEY_UP) { if (selectedIndex <= 0) selectedIndex = candidates.length - 1; else selectedIndex -= 1; } else { if (selectedIndex >= candidates.length - 1) selectedIndex = 0; else selectedIndex += 1; } updateCandidatesList(); preventDefaultEvent(e); // for IE break; case KEY_BS: if (! suggesting) break; // すべて消したら補完終了 var currentPos = getCaretPos(tagInput); if (getWordStartPos(tagInput) == currentPos) { suggesting = false; } selectedIndex = 0; break; case KEY_ENTER: // onpressで処理 break; case KEY_SHIFT: break; default: // selectedIndex = 0; } } function onKeyUpHandler(e) { candidates = new Array(); var currentPos = getCaretPos(tagInput); suggesting = true; // 候補を絞り込み var startPos = getWordStartPos(tagInput); if (startPos < currentPos) { var unfinishedWord = tagInput.value.substring(startPos, currentPos).toLowerCase(); for (var i = 0; i < tags.length; i++) { if (tags_lower[i].indexOf(unfinishedWord) == 0) { if (! isAdded(tags[i])) { candidates.push(tags[i]); } else { } } } } switch (e.keyCode) { case KEY_UP: case KEY_DOWN: // case KEY_LEFT: // case KEY_RIGHT: // case KEY_ENTER: // case KEY_TAB: case KEY_SHIFT: break; default: updateCandidatesList(); updateAllTagsLists(); updateCommentTags(); } } function updateAllTagsLists() { if (updateAddedTags()) { for (var i = 0; i < tags.length; i++) { if (isAdded(tags[i])) { selectTag(document.getElementById("tag" + i)); } else { unselectTag(document.getElementById("tag" + i)); } } for (var i = 0; i < otherTags.length; i++) { if (isAdded(otherTags[i])) { selectTag(document.getElementById("otherTag" + i)); } else { unselectTag(document.getElementById("otherTag" + i)); } } for (var i = 0; i < keywords.length; i++) { if (isAdded(keywords[i])) { selectTag(document.getElementById("keyword" + i)); } else { unselectTag(document.getElementById("keyword" + i)); } } } } // // 初期化 // function tag_suggest_init() { commentInput = unsafeWindow.document.getElementById("comment"); candidatesListDiv = unsafeWindow.document.getElementById("candidates_list"); userTagsListDiv = unsafeWindow.document.getElementById("tags_list"); otherTagsListDiv = unsafeWindow.document.getElementById("othertags_list"); keywordsListDiv = unsafeWindow.document.getElementById("keywords_list"); tagInput = unsafeWindow.document.createElement("input"); tagInput.setAttribute("id", "tag"); tagInput.setAttribute("size", 50); tagInput.setAttribute("autocomplete", "off" ); var td1 = document.createElement("td"); td1.setAttribute("class", "label"); td1.appendChild(document.createTextNode("タグ")); var td2 = document.createElement("td"); td2.appendChild(tagInput); var tr = document.createElement("tr"); tr.appendChild(td1); tr.appendChild(td2); var trComment = commentInput.parentNode.parentNode; trComment.parentNode.insertBefore(tr, trComment); if (unsafeWindow["tags"]) { tags = getNormalizedTags(unsafeWindow.tags); tags_lower = getLowerTags(tags); appendUserTagsList(); } if (unsafeWindow["otherTags"]) { otherTags = getNormalizedTags(unsafeWindow.otherTags); appendOtherTagsList(); } if (unsafeWindow["keywords"]) { keywords = getNormalizedTags(unsafeWindow.keywords); appendKeywordsList(); } tagInput.onkeydown = onKeyDownHandler; tagInput.onkeypress = onKeyPressHandler; tagInput.onkeyup = onKeyUpHandler; tagInput.focus(); candidatesListDiv.style.height = "0px"; unsafeWindow.updateCandidatesList = function() {}; if (commentInput.value.match(/^((\[[^\[\]]+?\])*)/)) { var s = RegExp.$1; s = s.replace(/\]\[/g, ", ").replace(/^\[/, "").replace(/\]$/, ", "); tagInput.value = s; updateAllTagsLists(); } } addEventListener("load", function() { if (document.getElementById("tag") == null) tag_suggest_init(); }, false); })();