// ==UserScript== // @name Append Tag Searching Tub // @namespace http://loda.jp/script/ // @id niconico-adds-search-tab-347021 // @version 4.0.0 // @description 『niconico』各サービスの検索窓について、「キーワード」「タグ」「マイリスト」「静画」「生放送」検索タブが5つとも含まれるように補完する / Adds "Keyword", "Tags", "My List", "Images" and "Live" search tabs to all of the Niconico search boxes. // @match http://www.nicovideo.jp/* // @match http://seiga.nicovideo.jp/* // @match http://live.nicovideo.jp/* // @match http://watch.live.nicovideo.jp/* // @match http://com.nicovideo.jp/* // @match http://blog.nicovideo.jp/en_info/* // @match http://tw.blog.nicovideo.jp/* // @match http://info.nicovideo.jp/psvita/en/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @domain www.nicovideo.jp // @domain seiga.nicovideo.jp // @domain live.nicovideo.jp // @domain watch.live.nicovideo.jp // @domain com.nicovideo.jp // @domain blog.nicovideo.jp // @domain tw.blog.nicovideo.jp // @domain info.nicovideo.jp // @run-at document-start // @icon data:image/vnd.microsoft.icon;base64,AAABAAEAMDAAAAEAIADMBwAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAwAAAAMAgGAAAAVwL5hwAAB5NJREFUaIHdWWtsHFcV/u6d2Zmd3Z21d9d2Y6+TtH4mKvUrdp6u5aaKooiqClUbHioESgUSf4qoqBAV0AqIAIk/4VEJUSSgpRIVtPyheQlQA4RGQJNiEeWFk9rUTt3C7sbenTs7M4cfu7Oe3ezDz9rlk6xZn3PvOd+555w7d+8CC8TQ4ABzP7fG41/d0t31Y/f/fffu5Qu1UwmDA/33t8bjx/t6e1vyIlZ1wlLw9ae+rDQ2NnyFMUaSJFF3d9dRV9fbc9eSHY4M7xnV9VACAAUCgT9v3dLVuSKES/HtI09HGxpiFwEQAJJlmbq7On+wHJt379m1NxwOp1ybgUCA+vt6D68E37I4sH9fTzQSGYcniK7OzmdcfVdX54IzMbx7576wrs+6tvx+P931gTu/tPKsS/DpT368PRotDaLjR65+7z2jNXti984d+3Vdt+AhP9Df93lXP7xn98r3gBcPPXCwOxaNXkUhCIk6OzoKjV2tJ3bt2H5A1/W0O1fz+6m3t+eLq0q4HA49+KGuWDR62SUiSRJ1drT/pNqcnduHPqjresado2ka9fX2PP5e8C0FB4CPfeShjlgsepExVgiio739p+6g1ni8kIkdQ4P366GQAQ/5/r7eQtncMzqyumVTCU88/li8IRa7wDAfRHtb28+9Y4YGBw6GgkFCYbfRaGhw22dd/ejI3WtD3sWjn/rE5oZYbIx5yqntjtufA4Bt/X336aGQDc9WuW2g/3NrSrgc/vj7Y3VNjY2nOecEgDjn1BpveTUUDL6NPPlgMJjas3vXR1fKZyFtflXpYVw6HItGBhlDyLIci0AAEYgAIgJRrgIIADkEx3FgWRYYY+A5zDHGNieSydtt26YSHzYAKazraVVVz5umCc65nA9soWy5xKWs4ziTgWDgpcnJf7/AACASqd8bi0Zf3j44oEeiEdiWDdM0IUwTpmnCNLMQQhTJhCFgGAYSqRQSiSRM0wRjLB8knDzx0romV8Y5d8cubrXzPhhj0PXQMwwANm5sPXHfgf37dmwfQjqdhmVZJIRgmYxBhmHAEDmyhjBgGAKGIZDJZCCEyYQpKJFI4tq165hLp0v9UZlnJW61ovHaAQDIsgQZAGLRyHBHexuEEAAAnyxDkiSoqgrTDOSIZ/KBCAEhBDIZA0IYEMJENBKBpmn4++vn4DjOolZ1CSgEalk2kwFAkmVNURRy65wAMMYgyzJkWWaaplI2GIQQJkQ+G2kjFwARMcuyaGr6RjnyrORZDrW2z3LZceeQDABERJzzwkDmqTMA4FyCquYyYttBZLMmDCHAAPw3kcSZ187i9XPna/BYURRKSfZIiqJkgNtkVBwMQygUQjgcxsVLl/DbV07Q2D8v1HJUVLuVyFTRV5TLFZQAY0VW3SBUVYVlWTh56nc4dvIUUqmbVXyvPrwBzNcaEcCY+2SUT4Xfr2J2dg4v/uolnP7Tmdwkj74MVqsHCvLyGZjnXhD5fDISiSR+9vwLOHf+HwX5YvfylYb3i4eHCQMDuW9fUhQFppnFi79+uYj8rfNuAS1izGL1BBQHUIz86kuSBFmW8YdXT+PMX87W8PXegwO59WaeWnTLIt+0bPzadRw7fqqSjYXU93J7oKKcA/mDGgiMzdc9EYFzDtM08crxE5idm6vhZ23AAcCyLNxM3STDELBtB5xzcM6hKAouXb5Cb7wxVs3GmvaADACmaWJq+gaCwSD8qgpN06CqCnw+H87+9W8QplnDx9pBBgAhBKamp1koGCJVVeD3+xHWdbw1NYUrV67WOinWOmV6n9XGLNb+/Hvg5uwsLl/5F25raoSiKPCrfszVp/HmxARm3nmnhv21RS4DhsD4+DWybRuaX4Xf74chDExMTsKy7KXWqKtb/bMQYwzJVApvTkygLhyGFtCQSs3ixtsz1amvAxQCAMCSyRSlMxkEtAAkzpDMHdTWfw94kTWzSJrJGjbXDyqchYqw3B5Y6JjF6muchd4n8AZQ9cxRBWt/Fno/4/+mB9b2ZngZ4AAgy5W/22P99gAAMA4Atm1fQPELY71mxMvLZiyfAUVVns0Leckg7wqWk7s/WLAyf5UIlBvnPQ+V81nqmwGQNU0bkwHg0KGDR59/7pd6OmM8rChKOwPBoflkMAYIYcI0zdIsEQDGOUcgoKE4idWQs+neepeQy99sMgSDAe+Nd2GeYztpLkkn6+vqnyxaqU0bW1u5JDXBcXyW4zhgADlE4bCeTKcz+6enb3wv79CBZwdr3rDhF41NDd96993/yJyzqg0F5K76gsFgKmtmh9+amn7WMAyvTQcAv62p8TfNLc1PzczMEOdcyV/zMM6YA8LsZx59ZPzJrz0tqjoqRbyl5Wj+1xcLQBYARSORC0984bHIogx5sGnTxm9IkkTI/QCSBUD1dXXXP/zgA61LtXkLNE1jAPDd7xwJ6aHQa8jXvs/nE1u3dN+7HNtE5AuH9VOuTVmWqL297eDyWZfgjs2bOQB0dXQMK4qS5ZxTU2PDN5dpUwKAO7du7VNVNcUYo4ZY9Psrwbcqmjds+GF9Xd3YI4cfDgHAyPCeZW+78ZbmI/V1dVdHR4YbAWB0ZHhBNv8HQF4nZ+TFtAIAAAAASUVORK5CYII= // @author 100の人 // @homepage https://greasyfork.org/ja/scripts/268-append-tag-searching-tub // @license Creative Commons Attribution 4.0 International Public License; http://creativecommons.org/licenses/by/4.0/ // @downloadURL none // ==/UserScript== (function () { 'use strict'; polyfill(); // L10N Gettext.setLocalizedTexts({ 'en': { 'キーワード': 'Keyword', '動画をキーワードで検索': 'Search Video by Keyword', 'タグ': 'Tags', '動画をタグで検索': 'Search Video by Tag', 'マイリスト': 'My List', 'マイリストを検索': 'Search My List', '静画': 'Images', '静画を検索': 'Search Images', '生放送': 'Live', '番組を探す': 'Search Live Program', '関連コミュニティ': 'Related Communities', 'このコミュニティを登録した人はこんなコミュニティも登録しています': 'People Who Joined This Community Also Joined', 'マンガ': 'Comics', }, 'zh': { 'キーワード': '關鍵字', '動画をキーワードで検索': '', 'タグ': '標籤', '動画をタグで検索': '', 'マイリスト': '我的清單', 'マイリストを検索': '搜尋我的清單', '静画': '靜畫', '静画を検索': '搜尋靜畫', '生放送': '生放送', '番組を探す': '搜尋節目', '関連コミュニティ': '相關社群', 'このコミュニティを登録した人はこんなコミュニティも登録しています': '加入此社群的人也有加入這些社群', 'マンガ': '漫畫', }, }); /** * タグ検索ページにおける検索窓の最大幅 (CSSの単位を含む)。 * @constant {string} */ var MAX_SEARCH_BOX_WIDTH = '268px'; /** * 追加したタブバーから新しいタブで検索結果を開いたとき、選択中のタブを元に戻す遅延時間 (ミリ秒)。 * @constant {number} */ var CURRENT_TAB_RESTORATION_DELAY = 1000; /** * 表示しているページのパスにスラッシュを前置し連結したもの。 * @type {Object} */ var pathname = window.location.pathname; /** * 表示しているページの種類。 * @type {string} */ var pageType; // ページの種類を取得 switch (window.location.host) { case 'www.nicovideo.jp': if (pathname === '/') { // 総合トップページ pageType = 'top'; } else if (pathname.startsWith('/search/')) { // 動画キーワード検索ページ pageType = 'videoSearch'; } else if (pathname.startsWith('/mylist_search')) { // マイリスト検索ページ pageType = 'mylist'; } else if (/^\/(?:(?:tag|related_tag|watch|mylist)\/|(?:recent|newarrival|hotlist|video_top|openlist|playlist|recommendations|video_catalog)(?:\/|$))/.test(pathname)) { // 動画タグ検索ページと原宿プレイヤーのページ pageType = 'tag'; } else if (pathname.startsWith('/user/')) { // ユーザーページ pageType = 'user'; } break; case 'seiga.nicovideo.jp': pageType = pathname.startsWith('/search/') // 静画検索ページ ? 'imageSearch' // 静画ページ : 'image'; break; case 'live.nicovideo.jp': case 'watch.live.nicovideo.jp': pageType = pathname.startsWith('/search') // 生放送検索ページ ? 'liveSearch' // 生放送ページ : 'live'; break; case 'com.nicovideo.jp': if (/^\/[^/]+\/co[0-9]+(?:\/|$)/.test(pathname)) { // コミュニティ詳細ページ pageType = 'community'; } break; case 'info.nicovideo.jp': if (pathname.startsWith('/psvita/en/')) { // 英語版PS Vita紹介ページ startScript(prepare, function (parent) { return parent.localName === 'body'; }, function (target) { return target.id === 'header'; }, function () { return document.getElementById('header'); }, { isTargetParent: function (parent) { return parent.localName === 'html'; }, isTarget: function (target) { return target.localName === 'body'; }, }); } return; case 'blog.nicovideo.jp': // 英語版ニコニコインフォ pageType = 'info_en'; break; case 'tw.blog.nicovideo.jp': // 台湾版ニコニコインフォ pageType = 'info_tw'; break; } // 上部メニューが追加されるまで待機 var targetParentIdFirefox, isTargetFirefox; switch (pageType) { case 'imageSearch': case 'image': isTargetFirefox = function (target) { return target.id === 'wrapper'; }; break; case 'liveSearch': targetParentIdFirefox = 'body_header'; break; case 'info_en': targetParentIdFirefox = 'container-inner'; break; case 'info_tw': targetParentIdFirefox = 'header'; break; } startScript(prepare, function (parent) { return parent.classList.contains('siteHeaderGlovalNavigation'); }, function (target) { return target.id === 'siteHeaderLeftMenu'; }, function () { return document.getElementById('siteHeaderLeftMenu'); }, { isTargetParent: targetParentIdFirefox ? function (parent) { return parent.id === targetParentIdFirefox; } : function (parent) { return parent.localName === 'body'; }, isTarget: isTargetFirefox || function (target) { return target.id === 'siteHeader'; }, }); /** * ページの種類別に、実行する関数を切り替える。 */ function prepare() { // ニコニコ生放送ではlang属性値が常にja-JPのため、ニコニコ動画へのリンク文字によって、ページの言語を判定する var textVideo = document.querySelector('[href^="http://www.nicovideo.jp/video_top"],[href^="http://nicovideo.jp/video_top"]').textContent; if (textVideo.contains('Video')) { Gettext.setLocale('en'); } else if (textVideo.contains('動畫')) { Gettext.setLocale('zh'); } if (!document.querySelector(pageType === 'imageSearch' ? '#siteHeader [href="/?header"], #siteHeader [href="/"]' : '#siteHeader [href^="http://seiga.nicovideo.jp/"], #globalNav [href^="http://seiga.nicovideo.jp/"]')) { // ヘッダに静画へのリンクが無ければ // 生放送へのリンクを取得 var itemLive = document.querySelector('#siteHeader [href^="http://live.nicovideo.jp/"], #globalNav [href^="http://live.nicovideo.jp/"]').parentNode; // 生放送リンクの複製 var item = itemLive.cloneNode(true); // リンク文字を変更 (item.getElementsByTagName('span')[0] || item.getElementsByTagName('a')[0]).textContent = _('静画'); // アドレスを変更 item.getElementsByTagName('a')[0].host = 'seiga.nicovideo.jp'; // ヘッダに静画へのリンクを追加 itemLive.parentNode.insertBefore(item, itemLive); } switch (pageType) { case 'videoSearch': // 動画キーワード startScript(addTagSearchTabAboveSearchBox, function (parent) { return parent.classList.contains('formSearch'); }, function (target) { return target.id === 'search_united_form'; }, function () { return document.getElementById('search_united_form'); }, { isTargetParent: function (parent) { return parent.localName === 'body'; }, isTarget: function (target) { return target.localName === 'section'; }, }); break; case 'mylist': // マイリスト startScript(addTagSearchTabAboveSearchBox, function (parent) { return parent.id === 'form_search'; }, function (target) { return target.id === 'search_united_form'; }, function () { return document.getElementById('search_united_form'); }, { isTargetParent: function (parent) { return parent.id === 'PAGEMAIN'; }, isTarget: function (target) { return target.id === 'PAGEBODY'; }, }); break; case 'top': // トップページ startScript(addTagSearchButtonToTopPage, function (parent) { return parent.id === 'searchFormInner'; }, function (target) { return target.id === 'searchForm'; }, function () { return document.getElementById('searchForm'); }, { isTargetParent: function (parent) { return parent.id === 'main_container' || parent.localName === 'body'; }, isTarget: function (target) { return target.id === 'searchFormWrap'; }, }); break; case 'imageSearch': // 静画キーワード startScript(addTagSearchTabAboveSearchBox, function (parent) { return parent.id === 'usearch_form'; }, function (target) { return target.id === 'usearch_form_input'; }, function () { return document.getElementById('usearch_form_input'); }, { isTargetParent: function (parent) { return parent.id === 'wrapper'; }, isTarget: function (target) { return target.id === 'main'; }, }); break; case 'image': // 静画 startScript(careteTabsBarToSearchBox, function (parent) { return parent.id === 'head_search_form'; }, function (target) { return target.id === 'search_button'; }, function () { return document.getElementById('search_button'); }, { isTargetParent: function (parent) { return parent.id === 'header_block'; }, isTarget: function () { return true; }, }); break; case 'liveSearch': // 生放送キーワード startScript(addTagSearchTabAboveSearchBox, function (parent) { return parent.classList.contains('container'); }, function (target) { return target.id === 'form_frm_btm'; }, function () { return document.getElementById('form_frm_btm'); }); break; case 'live': // 生放送 startScript(careteTabsBarToSearchBox, function (parent) { return parent.classList.contains('search_program'); }, function (target) { return target.classList.contains('search_word'); }, function () { return document.getElementsByClassName('search_word')[0]; }, { isTargetParent: function (parent) { return parent.localName === 'body'; }, isTarget: function (target) { return target.id === 'page_header'; }, }); break; case 'tag': if (document.doctype.publicId) { // 原宿プレイヤーの動画ページ、オススメ動画ページ等 startScript(addOtherServiceTabsAboveSearchBox, function (parent) { return parent.id === 'search_tab'; }, function (target) { return target.id === 'target_m'; }, function () { return document.getElementById('target_m'); }, { isTargetParent: function (parent) { return parent.id === 'PAGEMAIN'; }, isTarget: function (target) { return target.id === 'PAGEBODY'; }, }); } else { // 動画タグ startScript(addOtherServiceTabsAboveSearchBox, function (parent) { return parent.classList.contains('videoSearchOption'); }, function (target) { return target.classList.contains('optMylist'); }, function () { return document.getElementsByClassName('optMylist')[0]; }, { isTargetParent: function (parent) { return parent.localName === 'body'; }, isTarget: function (target) { return target.localName === 'header'; }, }); return; } break; case 'user': // ユーザー startScript(addImageLinkToUserPageMenu, function (parent) { return parent.localName === 'body'; }, function (target) { return target.classList.contains('optionOuter'); }, function () { return document.getElementsByClassName('optionOuter')[0]; }); break; case 'community': // コミュニティ startScript(addRelatedCommunitiesLink, function (parent) { return parent.localName === 'body'; }, function (target) { return target.id === 'site-body'; }, function () { return document.getElementById('site-body'); }); break; } } /** * 各サービスのキーワード検索ページの検索窓に、動画の「タグ」検索タブを追加する。 */ function addTagSearchTabAboveSearchBox() { // マイリスト検索タブの取得 var mylistTab = document.querySelector('.tab_table td:nth-of-type(2), #search_frm_a a:nth-of-type(2), .seachFormA a:nth-of-type(2)'); // マイリスト検索タブの複製 var tagTab = mylistTab.cloneNode(true); // タブ名を変更 var anchor = tagTab.tagName.toLowerCase() === 'a' ? tagTab : tagTab.getElementsByTagName('a')[0]; var tabNameNode = anchor.getElementsByTagName('div'); tabNameNode = (tabNameNode.length > 0 ? tabNameNode[0].firstChild : anchor.firstChild); tabNameNode.data = _('タグ') + (pageType === 'liveSearch' ? '(' : ' ( '); // クラス名を変更・動画件数をリセット var searchCount = tagTab.querySelector('strong, span'); switch (pageType) { case 'videoSearch': searchCount.classList.remove('more'); break; case 'mylist': searchCount.style.removeProperty('color'); break; case 'imageSearch': searchCount.classList.remove('search_value_em'); searchCount.classList.add('search_value'); break; case 'liveSearch': searchCount.classList.remove('Redtxt'); break; } searchCount.textContent = '-'; if (searchCount.id) { // 生放送 searchCount.id = 'search_count_tag'; } // 検索語句を取得 var searchWordsPattern = /(?:\/(?:search|tag|mylist_search)\/|[?&]keyword=)([^?&#]+)/g; var result = window.location.href.match(searchWordsPattern); var searchWords = result ? searchWordsPattern.exec(result[pageType === 'liveSearch' ? result.length - 1 : 0])[1] : ''; // タグが付いた動画件数を取得・表示 if (searchWords) { GM_xmlhttpRequest({ method: 'GET', url: 'http://www.nicovideo.jp/tag/' + searchWords, onload: function (response) { var responseDocument = new DOMParser().parseFromString(response.responseText, 'text/html'); var total = responseDocument.querySelector('.tagCaption .dataValue .num').textContent; var trimmedThousandsSep = total.replace(/,/g, ''); if (trimmedThousandsSep >= 100) { // 動画件数が100件を超えていれば switch (pageType) { case 'videoSearch': searchCount.classList.add('more'); break; case 'mylist': searchCount.style.color = '#CC0000'; break; case 'imageSearch': searchCount.classList.remove('search_value'); searchCount.classList.add('search_value_em'); break; case 'liveSearch': searchCount.classList.add('Redtxt'); break; } } switch (pageType) { case 'mylist': searchCount.textContent = ' ' + total + ' '; break; case 'videoSearch': case 'imageSearch': searchCount.textContent = total; break; case 'liveSearch': searchCount.textContent = trimmedThousandsSep; break; } } }); } // 非アクティブタブを取得 var inactiveTab = document.querySelector('.tab_0, .tab1'); // クラス名を変更 anchor.className = inactiveTab.className; // アドレスを変更 anchor.href = 'http://www.nicovideo.jp/tag/' + searchWords + inactiveTab.search; // タグ検索タブを追加 mylistTab.parentNode.insertBefore(tagTab, mylistTab); if (inactiveTab.classList.contains('tab1')) { // GINZAバージョン mylistTab.parentNode.insertBefore(tagTab.previousSibling.cloneNode(true), mylistTab); } } /** * ニコニコ動画の上部に表示されている検索窓に、「静画」「生放送」を検索するタブを追加する。 */ function addOtherServiceTabsAboveSearchBox() { // スタイルの設定 document.head.insertAdjacentHTML('beforeend', ''); // タブリストの取得 var mylistTab = document.querySelector('#target_m, .optMylist'); var tabList = mylistTab.parentNode; // タブの複製・追加 [ { type: 'image', title: _('静画を検索'), uri: 'http://seiga.nicovideo.jp/search', text: _('静画'), }, { type: 'live', title: _('番組を探す'), uri: 'http://live.nicovideo.jp/search', text: _('生放送'), }, ].forEach(function (option) { var tab = mylistTab.cloneNode(true); if (mylistTab.classList.contains('optMylist')) { // GINZAバージョン tab.classList.remove('optMylist'); tab.classList.add('opt' + option.type[0].toUpperCase() + option.type.slice(1)); tab.dataset.type = option.type; tab.getElementsByTagName('a')[0].textContent = option.text; } else { // 原宿バージョン tab.id = 'target_' + option.type[0]; tab.title = option.title; tab.setAttribute('onclick', tab.getAttribute('onclick').replace(/'.+?'/, '\'' + option.uri + '\'')); tab.textContent = option.text; } tabList.appendChild(tab); }); if (mylistTab.classList.contains('optMylist')) { // GINZAバージョン var script = document.createElement('script'); script.text = '(' + (function () { eval('Nico.Navigation.HeaderSearch.Controller.search = ' + Nico.Navigation.HeaderSearch.Controller.search.toString().replace(/(switch.+?{.+?)(})/, '$1; break;' + 'case "image":' + 'd = "http://seiga.nicovideo.jp/search/" + e; break;' + 'case "live":' + 'd = "http://live.nicovideo.jp/search/" + e; break;' + '$2')); }).toString() + ')();'; document.head.appendChild(script); } } /** * 静画・生放送の上部に表示されている検索窓に、「動画キーワード」「動画タグ」「マイリスト」「静画」「生放送」を検索するタブバーを設置する。 */ function careteTabsBarToSearchBox() { // スタイルの設定 document.head.insertAdjacentHTML('beforeend', ''); /** * 静画検索のtargetパラメータの値。 * @type {string} */ var imageSearchParamValue = 'illust'; /** * 静画検索タブの補足情報・プレースホルダー。 * @type {string} */ var imageSearchPlaceholder = 'イラストを検索'; var form = document.querySelector('[action$="search"]'); var textField = form[pageType === 'image' ? 'q' : 'keyword']; if (pageType === 'image') { // 静画の場合 var pathnameParts = document.querySelector('#logo > h1 > a').pathname.split('/'); switch (pathnameParts[1]) { case 'manga': imageSearchParamValue = 'manga'; break; case 'book': imageSearchParamValue = pathnameParts[2] === 'r18' ? 'book_r18' : 'book'; break; } imageSearchPlaceholder = textField.defaultValue; } form.insertAdjacentHTML('afterbegin', ''); var defaultCurrentTabAnchor = form.querySelector('.current a'); document.addEventListener('click', function (event) { var button = event.button, target = event.target; if (button !== 2 && target['matches' in target ? 'matches' : /* Firefox */ 'mozMatchesSelector']('[action$="search"] > ul > li > a')) { // タブが副ボタン以外でクリックされたとき var searchWord = textField.value.trim(); if (pageType === 'image' && textField.value === textField.defaultValue) { // 静画の場合、検索窓の値が既定値と一致していれば空欄とみなす searchWord = ''; } if (searchWord) { // 検索語句が入力されていれば switchTab(target); target.pathname = target.pathname.replace(/[^/]*$/, encodeURIComponent(searchWord)); window.setTimeout(function () { // リンク先を新しいタブで開いたとき switchTab(defaultCurrentTabAnchor); }, CURRENT_TAB_RESTORATION_DELAY); } else { // 検索語句が未入力なら event.preventDefault(); if (button === 0) { // 主ボタンでクリックされていれば switchTab(target); } } } }); // TabSubmitをインストールしているとマウスボタンを取得できず、中クリック時にも同じタブで検索してしまうため分割 form.addEventListener('click', function (event) { if (event.target.type === (pageType === 'image' ? 'image' : 'submit')) { // 送信ボタンをクリックしたとき var searchWord = textField.value !== textField.defaultValue && textField.value.trim(); if (searchWord) { event.stopPropagation(); event.preventDefault(); var anchor = form.querySelector('.current a'); anchor.pathname = anchor.pathname.replace(/[^/]*$/, encodeURIComponent(searchWord)); window.location.assign(anchor.href); } } }, true); window.addEventListener('pageshow', function (event) { if (event.persisted) { // 履歴にキャッシュされたページを再表示したとき switchTab(defaultCurrentTabAnchor); } }); /** * 選択しているタブを切り替える。 * @param {HTMLAnchorElement} target - 切り替え先のタブのリンク。 */ function switchTab(target) { form.getElementsByClassName('current')[0].classList.remove('current'); target.parentElement.classList.add('current'); if (pageType === 'image') { // 静画 if (textField.defaultValue === textField.value) { // 検索語句が未入力なら textField.defaultValue = textField.value = target.title; } else { // 検索語句が入力されていれば textField.defaultValue = target.title; } } else { // 生放送 textField.placeholder = target.title; } } } /** * 総合トップページの検索窓に、動画「タブ」検索ボタンを追加する。 */ function addTagSearchButtonToTopPage() { fixPrototypeJavaScriptFramework(); // スタイルの設定 document.head.insertAdjacentHTML('beforeend', ''); // マイリスト検索ボタンの取得 var refItem = document.getElementsByClassName('sMylist')[0].parentNode; // マイリスト検索ボタンの複製 var item = refItem.cloneNode(true); // ボタン名を変更 var anchor = item.getElementsByTagName('a')[0]; anchor.textContent = _('タグ'); // クラス名を変更 anchor.className = 'sVideo'; // アドレスを変更 anchor.href = 'http://www.nicovideo.jp/tag/'; // タグ検索ボタンを追加 refItem.parentNode.insertBefore(item, refItem); if (!document.getElementsByClassName('sSeiga')[0]) { // 静画検索ボタンが存在しなければ // 生放送検索の取得 refItem = document.getElementsByClassName('sLive')[0].parentNode; // 生放送検索の複製 item = refItem.cloneNode(true); // ボタン名を変更 anchor = item.getElementsByTagName('a')[0]; anchor.textContent = _('静画'); // クラス名を変更 anchor.className = 'sSeiga'; // アドレスを変更 anchor.href = 'http://seiga.nicovideo.jp/search/'; // 静画検索を追加 refItem.parentNode.insertBefore(item, refItem); startScript(function () { var list, item, anchor; // メニューの生放送リンクの取得 list = document.querySelector('.service_main .live').parentNode.parentNode; // 生放送リンクの複製 item = list.cloneNode(true); // リンク文字を変更 anchor = item.getElementsByTagName('a')[0]; anchor.title = anchor.textContent = _('静画'); // クラス名を変更 anchor.classList.remove('live'); anchor.classList.add('seiga'); // アドレスを変更 item.getElementsByTagName('a')[0].href = 'http://seiga.nicovideo.jp/'; // メニューに静画へのリンクを追加 list.parentNode.insertBefore(item, list); // サブメニューの複製 item = document.getElementsByClassName('service_sub')[0].cloneNode(true); // 2つ目以降の要素を削除 Array.prototype.forEach.call(item.querySelectorAll('li:first-child ~ li'), function (item) { item.parentNode.removeChild(item); }); // リンク文字を変更 anchor = item.getElementsByTagName('a')[0]; anchor.title = anchor.textContent = _('マンガ'); // アドレスを変更 item.getElementsByTagName('a')[0].href = 'http://seiga.nicovideo.jp/manga/'; // メニューに静画のサブメニューへのリンクを追加 list.parentNode.insertBefore(item, list); }, function (parent) { return parent.id === 'sideNav'; }, function (target) { return target.id === 'trendyTags'; }, function () { return document.querySelector('#menuService [href="http://live.nicovideo.jp/timetable/"]'); }, { isTarget: function (target) { return target.id === 'NewServiceList'; }, }); } } /** * ユーザーページ左側のメニューに、静画へのリンクを追加する。 */ function addImageLinkToUserPageMenu() { // スタイルの設定 document.head.insertAdjacentHTML('beforeend', ''); var nextItem = document.getElementsByClassName('stampTab')[0]; var item = nextItem.cloneNode(true); var classList = item.classList; classList.remove('stampTab'); classList.remove('active'); classList.add('imageTab'); var anchor = item.getElementsByTagName('a')[0]; var userId = /[0-9]+/.exec(anchor.pathname)[0]; anchor.href = 'http://seiga.nicovideo.jp/user/illust/' + userId; anchor.lastChild.data = _('静画'); var nextItem = document.getElementsByClassName('stampTab')[0]; nextItem.parentElement.insertBefore(item, nextItem); } /** * コミュニティ詳細ページのメニューに、関連コミュニティページ (「コミュニティを登録しました」ページ) へのリンクを追加する。 */ function addRelatedCommunitiesLink() { var result = /^\/motion\/(co[0-9]+)\/done(?:\/|$)/.exec(window.location.pathname); /** * 関連コミュニティページを開いていれば真。 * @type {boolean} */ var relatedCommunitiesPage = result; if (relatedCommunitiesPage) { // 関連コミュニティページなら var joinedCommunityPage = false; var lastestJoinedCommunity = GM_getValue('lastestJoinedCommunity'); if (lastestJoinedCommunity) { // コミュニティ登録ボタンを押した後なら GM_deleteValue('lastestJoinedCommunity'); if (result[1] === lastestJoinedCommunity) { // 「コミュニティを登録しました」を表示すべきなら joinedCommunityPage = true; } } if (!joinedCommunityPage) { // 「コミュニティを登録しました」の表示を消す document.head.insertAdjacentHTML('beforeend', ''); } } // スタイルの設定 document.head.insertAdjacentHTML('beforeend', ''); // メニューにリンクを追加 var topLinkItem = document.getElementById('btn_top'); var item = topLinkItem.cloneNode(true); item.id = 'btn_rlt'; item.title = _('このコミュニティを登録した人はこんなコミュニティも登録しています'); var anchor = item.firstElementChild; anchor.pathname = anchor.pathname.replace('community', 'motion') + '/done' anchor.classList[relatedCommunitiesPage ? 'add' : 'remove']('on'); anchor.firstElementChild.textContent = _('関連コミュニティ'); topLinkItem.parentElement.appendChild(item); // コミュニティ登録時にコミュニティIDを記録する var form = document.querySelector('form[action^="/motion/co"]'); if (form) { // コミュニティ登録確認ページなら form.addEventListener('submit', function (event) { GM_setValue('lastestJoinedCommunity', form.action.replace('http://com.nicovideo.jp/motion/', '')); }); } } /** * 挿入された節の親節が、目印となる節の親節か否かを返すコールバック関数。 * @callback isTargetParent * @param {(Document|Element)} parent * @returns {boolean} */ /** * 挿入された節が、目印となる節か否かを返すコールバック関数。 * @callback isTarget * @param {(DocumentType|Element)} target * @returns {boolean} */ /** * 目印となる節が文書に存在するか否かを返すコールバック関数。 * @callback existsTarget * @returns {boolean} */ /** * 目印となる節が挿入された直後に関数を実行する。 * @param {Function} main - 実行する関数。 * @param {isTargetParent} isTargetParent * @param {isTarget} isTarget * @param {existsTarget} existsTarget * @param {Object} [callbacksForFirefox] * @param {isTargetParent} [callbacksForFirefox.isTargetParent] - Firefoxにおける{@link isTargetParent}。 * @param {isTarget} [callbacksForFirefox.isTarget] - Firefoxにおける{@link isTarget}。 * @param {boolean} [timeoutSinceStopParsingDocument=0] - DOM構築完了後に監視を続けるミリ秒数。 * @version 2014-09-04 */ function startScript(main, isTargetParent, isTarget, existsTarget) { /** * {@link checkExistingTarget}で{@link startMain}を実行する間隔(ミリ秒)。 * @constant {number} */ var INTERVAL = 10; /** * {@link checkExistingTarget}で{@link startMain}を実行する回数。 * @constant {number} */ var LIMIT = 500; /** * 実行済みなら真。 * @type {boolean} */ var alreadyCalled = false; // 指定した節が既に存在していれば、即実行 startMain(); if (alreadyCalled) { return; } // FirefoxのMutationObserverは、HTMLのDOM構築に関して要素をまとめて挿入したと見なすため、isTargetParent、isTargetを変更 var callbacksForFirefox = arguments[4]; if (callbacksForFirefox && typeof MozSettingsEvent !== 'undefined') { isTargetParent = callbacksForFirefox.isTargetParent || isTargetParent; isTarget = callbacksForFirefox.isTarget || isTarget; } var observer = new MutationObserver(mutationCallback); observer.observe(document, { childList: true, subtree: true, }); var timeoutSinceStopParsingDocument = arguments[5] || 0; if (document.readyState === 'complete') { // DOMの構築が完了していれば onDOMContentLoaded(); } else { document.addEventListener('DOMContentLoaded', onDOMContentLoaded); } /** * {@link startMain}を実行し、スクリプトが開始されていなければさらに{@link timeoutSinceStopParsingDocument}ミリ秒待機し、 * スクリプトが開始されていなければ{@link stopObserving}を実行する。 */ function onDOMContentLoaded() { startMain(); if (timeoutSinceStopParsingDocument === 0) { if (!alreadyCalled) { stopObserving(); } } else { window.setTimeout(function () { if (!alreadyCalled) { stopObserving(); } }, timeoutSinceStopParsingDocument); } } /** * 目印となる節が挿入されたら、監視を停止し、{@link checkExistingTarget}を実行する。 * @param {MutationRecord[]} mutations - A list of MutationRecord objects. * @param {MutationObserver} observer - The constructed MutationObserver object. */ function mutationCallback(mutations, observer) { var mutation, target, nodeType, addedNodes, addedNode, i, j, l, l2; for (i = 0, l = mutations.length; i < l; i++) { mutation = mutations[i]; target = mutation.target; nodeType = target.nodeType; if ((nodeType === Node.ELEMENT_NODE) && isTargetParent(target)) { // 子が追加された節が要素節で、かつその節についてisTargetParentが真を返せば addedNodes = Array.prototype.slice.call(mutation.addedNodes); for (j = 0, l2 = addedNodes.length; j < l2; j++) { addedNode = addedNodes[j]; nodeType = addedNode.nodeType; if ((nodeType === Node.ELEMENT_NODE) && isTarget(addedNode)) { // 追加された子が要素節で、かつその節についてisTargetが真を返せば observer.disconnect(); checkExistingTarget(0); return; } } } } } /** * {@link startMain}を実行し、スクリプトが開始されていなければ再度実行。 * @param {number} count - {@link startMain}を実行した回数。 */ function checkExistingTarget(count) { startMain(); if (!alreadyCalled && count < LIMIT) { window.setTimeout(checkExistingTarget, INTERVAL, count + 1); } } /** * 指定した節が存在するか確認し、存在すれば{@link stopObserving}を実行しスクリプトを開始。 */ function startMain() { if (!alreadyCalled && existsTarget()) { stopObserving(); main(); } } /** * 監視を停止する。 */ function stopObserving() { alreadyCalled = true; if (observer) { observer.disconnect(); } document.removeEventListener('DOMContentLoaded', onDOMContentLoaded); } } /** * prototype汚染が行われる Prototype JavaScript Framework (prototype.js) 1.5.1.1 のバグを修正(Tampermonkey用) */ function fixPrototypeJavaScriptFramework() { [ [document, 'getElementsByClassName'], ].forEach(function (objectProperty) { delete objectProperty[0][objectProperty[1]]; }); } /** * 国際化・地域化関数の読み込み、ECMAScript仕様のPolyfill。 */ function polyfill() { /** * DOM関連のメソッド。 */ var DOMUtils = { /** * XMLの特殊文字を文字参照に置換する。 * @param {string} str - プレーンな文字列。 * @returns {string} HTMLとして扱われる文字列。 */ convertSpecialCharactersToCharacterReferences: function (str) { return str.replace(/[&<>"']/g, function (specialCharcter) { return '&#x' + specialCharcter.charCodeAt(0).toString(16) + ';' }); }, }; window.h = DOMUtils.convertSpecialCharactersToCharacterReferences; /** * 以下のような形式の翻訳リソース。すべての言語について、msgidは欠けていないものとする。 * {@link Gettext.DEFAULT_LOCALE}のリソースを必ず含む。{@link Gettext.ORIGINAL_LOCALE}のリソースは無視される。 * { * 'IETF言語タグ': { * '翻訳前 (msgid)': '翻訳後 (msgstr)', * …… * }, * …… * } * @typedef {Object} LocalizedTexts */ /** * i18n。 * @version 2014-07-10 */ window.Gettext = { /** * 翻訳対象文字列 (msgid) の言語。IETF言語タグの「language」サブタグ。 * @constant {string} */ ORIGINAL_LOCALE: 'ja', /** * クライアントの言語の翻訳リソースが存在しないとき、どの言語に翻訳するか。IETF言語タグの「language」サブタグ。 * @constant {string} */ DEFAULT_LOCALE: 'en', /** * 翻訳リソースを追加する。 * @param {LocalizedTexts} localizedTexts */ setLocalizedTexts: function (localizedTexts) { this.multilingualLocalizedTexts = localizedTexts; }, /** * クライアントの言語を設定する。 * @param {string} clientLang - IETF言語タグ(「language」と「language-REGION」にのみ対応)。 */ setLocale: function (clientLang) { var splitedClientLang = clientLang.split('-', 2); this.language = splitedClientLang[0].toLowerCase(); this.langtag = this.language + (splitedClientLang[1] ? '-' + splitedClientLang[1].toUpperCase() : ''); if (this.language === 'ja') { // ja-JPをjaと同一視 this.langtag = this.language; } }, /** * テキストをクライアントの言語に変換する。 * @param {string} message - 翻訳前。 * @returns {string} 翻訳後。 */ gettext: function (message) { // クライアントの言語が翻訳元の言語なら、そのまま返す return this.langtag === this.ORIGINAL_LOCALE && message // クライアントの言語の翻訳リソースが存在すれば、それを返す || this.langtag in this.multilingualLocalizedTexts && this.multilingualLocalizedTexts[this.langtag][message] // 地域下位タグを取り除いた言語タグの翻訳リソースが存在すれば、それを返す || this.language in this.multilingualLocalizedTexts && this.multilingualLocalizedTexts[this.language][message] // 既定言語の翻訳リソースが存在すれば、それを返す || this.DEFAULT_LOCALE in this.multilingualLocalizedTexts && this.multilingualLocalizedTexts[this.DEFAULT_LOCALE][message] // そのまま返す || message; }, /** * クライアントの言語。{@link Gettext.setLocale}から変更される。 * @type {string} * @access private */ langtag: 'ja', /** * クライアントの言語のlanguage部分。{@link Gettext.setLocale}から変更される。 * @type {string} * @access private */ language: 'ja', /** * 翻訳リソース。{@link Gettext.setLocalizedTexts}から変更される。 * @type {LocalizedTexts} * @access private */ multilingualLocalizedTexts: {}, }; window._ = Gettext.gettext.bind(Gettext); // Polyfill for Opera and Google Chrome if (!''.startsWith) { /** * Determines whether a string begins with the characters of another string, returning true or false as appropriate. * @param {string} searchString - The characters to be searched for at the start of this string. * @param {number} [position=0] - The position in this string at which to begin searching for searchString. * @returns {boolean} * @see {@link http://people.mozilla.org/~jorendorff/es6-draft.html#sec-string.prototype.startswith 21.1.3.18 String.prototype.startsWith (searchString [, position ] )} * @see {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith String.startsWith - JavaScript | MDN} * @version polyfill-2013-11-05 * @name String.prototype.startsWith */ Object.defineProperty(String.prototype, 'startsWith', { writable: true, enumerable: false, configurable: true, value: function (searchString) { var position = arguments[1]; return this.indexOf(searchString, position) === Math.max(Math.floor(position) || 0, 0); }, }); /** * Determines whether one string may be found within another string, returning true or false as appropriate. * @param {string} searchString - A string to be searched for within this string. * @param {number} [position=0] - The position in this string at which to begin searching for searchString. * @returns {boolean} * @see {@link http://people.mozilla.org/~jorendorff/es6-draft.html#sec-string.prototype.contains 21.1.3.6 String.prototype.contains (searchString, position = 0 )} * @see {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/contains String.contains - JavaScript | MDN} * @version polyfill-2013-11-05 * @name String.prototype.contains */ Object.defineProperty(String.prototype, 'contains', { writable: true, enumerable: false, configurable: true, value: function (searchString) { return this.indexOf(searchString, arguments[1]) !== -1; }, }); } } })();