Append Tag Searching Tub

Adds “Keyword”, “Tags”, “My List”, “Images” and “Live” search tabs to all of the Niconico search boxes.

当前为 2020-01-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Append Tag Searching Tub
  3. // @name:ja niconico タグ検索タブを追加
  4. // @description Adds “Keyword”, “Tags”, “My List”, “Images” and “Live” search tabs to all of the Niconico search boxes.
  5. // @description:ja 『niconico』各サービスの検索窓について、「キーワード」「タグ」「マイリスト」「静画」「生放送」検索タブが5つとも含まれるように補完します。
  6. // @namespace http://loda.jp/script/
  7. // @version 5.2.0
  8. // @match https://www.nicovideo.jp/
  9. // @match https://www.nicovideo.jp/?*
  10. // @match https://www.nicovideo.jp/#*
  11. // @match https://www.nicovideo.jp/tag/*
  12. // @match https://www.nicovideo.jp/related_tag/*
  13. // @match https://www.nicovideo.jp/mylist*
  14. // @match https://www.nicovideo.jp/search/*
  15. // @match *://seiga.nicovideo.jp/*
  16. // @match https://live.nicovideo.jp/*
  17. // @match https://com.nicovideo.jp/*
  18. // @match *://blog.nicovideo.jp/en_info/*
  19. // @match *://tw.blog.nicovideo.jp/*
  20. // @require https://gitcdn.xyz/cdn/greasemonkey/gm4-polyfill/a834d46afcc7d6f6297829876423f58bb14a0d97/gm4-polyfill.js
  21. // @require https://greasyfork.org/scripts/17895/code/polyfill.js?version=625392
  22. // @require https://greasyfork.org/scripts/19616/code/utilities.js?version=230651
  23. // @require https://greasyfork.org/scripts/17896/code/start-script.js?version=112958
  24. // @license MPL-2.0
  25. // @compatible Edge 非推奨 / Deprecated
  26. // @compatible Firefox
  27. // @compatible Opera
  28. // @compatible Chrome
  29. // @grant GM.setValue
  30. // @grant GM_setValue
  31. // @grant GM.getValue
  32. // @grant GM_getValue
  33. // @grant GM.deleteValue
  34. // @grant GM_deleteValue
  35. // @grant GM.xmlHttpRequest
  36. // @grant GM_xmlhttpRequest
  37. // @connect www.nicovideo.jp
  38. // @run-at document-start
  39. // @icon data:image/vnd.microsoft.icon;base64,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
  40. // @author 100の人
  41. // @homepageURL https://greasyfork.org/scripts/268
  42. // ==/UserScript==
  43.  
  44. 'use strict';
  45.  
  46. // L10N
  47. Gettext.setLocalizedTexts({
  48. /*eslint-disable quote-props, max-len */
  49. 'en': {
  50. 'キーワード': 'Keyword',
  51. '動画をキーワードで検索': 'Search Video by Keyword',
  52. 'タグ': 'Tags',
  53. '動画をタグで検索': 'Search Video by Tag',
  54. 'マイリスト': 'My List',
  55. 'マイリストを検索': 'Search My List',
  56. '静画': 'Images',
  57. '静画を検索': 'Search Images',
  58. '生放送': 'Live',
  59. '番組を探す': 'Search Live Program',
  60. 'マンガ': 'Comics',
  61. },
  62. 'zh': {
  63. 'キーワード': '關鍵字',
  64. '動画をキーワードで検索': '',
  65. 'タグ': '標籤',
  66. '動画をタグで検索': '',
  67. 'マイリスト': '我的清單',
  68. 'マイリストを検索': '搜尋我的清單',
  69. '静画': '靜畫',
  70. '静画を検索': '搜尋靜畫',
  71. '生放送': '生放送',
  72. '番組を探す': '搜尋節目',
  73. 'マンガ': '漫畫',
  74. },
  75. /*eslint-enable quote-props, max-len */
  76. });
  77.  
  78.  
  79.  
  80. /**
  81. * 追加したタブバーから新しいタブで検索結果を開いたとき、選択中のタブを元に戻す遅延時間 (ミリ秒)。
  82. * @constant {number}
  83. */
  84. const CURRENT_TAB_RESTORATION_DELAY = 1000;
  85.  
  86. /**
  87. * 表示しているページの種類。
  88. * @type {string}
  89. */
  90. let pageType;
  91.  
  92. // ページの種類を取得
  93. switch (location.host) {
  94. case 'www.nicovideo.jp':
  95. if (location.pathname === '/') {
  96. // 総合トップページ
  97. pageType = 'top';
  98. } else if (location.pathname.startsWith('/search/')) {
  99. // 動画キーワード検索ページ
  100. pageType = 'videoSearch';
  101. } else if (location.pathname.startsWith('/mylist_search')) {
  102. // マイリスト検索ページ
  103. pageType = 'mylist';
  104. } else if (/^\/(?:(?:tag|related_tag)\/|(?:mylist|recent|newarrival|openlist|video_catalog)(?:\/|$))/
  105. .test(location.pathname)) {
  106. // 動画タグ検索ページと公開マイリスト等
  107. pageType = 'tag';
  108. } else if (location.pathname.startsWith('/user/')) {
  109. // ユーザーページ
  110. pageType = 'user';
  111. }
  112. break;
  113. case 'seiga.nicovideo.jp':
  114. pageType = location.pathname.startsWith('/search/')
  115. // 静画検索ページ
  116. ? 'imageSearch'
  117. // 静画ページ
  118. : 'image';
  119. break;
  120. case 'live.nicovideo.jp':
  121. pageType = location.pathname.startsWith('/search')
  122. // 生放送検索ページ
  123. ? 'liveSearch'
  124. // 生放送ページ
  125. : 'live';
  126. break;
  127. case 'blog.nicovideo.jp':
  128. // 英語版ニコニコインフォ
  129. pageType = 'info_en';
  130. break;
  131. case 'tw.blog.nicovideo.jp':
  132. // 台湾版ニコニコインフォ
  133. pageType = 'info_tw';
  134. break;
  135. }
  136.  
  137. // 上部メニューが追加されるまで待機
  138. let targetParentIdFirefox, isTargetFirefox;
  139. switch (pageType) {
  140. case 'imageSearch':
  141. case 'image':
  142. isTargetFirefox = target => target.id === 'wrapper';
  143. break;
  144. case 'liveSearch':
  145. targetParentIdFirefox = 'body_header';
  146. break;
  147. case 'info_en':
  148. targetParentIdFirefox = 'container-inner';
  149. break;
  150. case 'info_tw':
  151. targetParentIdFirefox = 'header';
  152. break;
  153. }
  154. startScript(
  155. prepare,
  156. parent => parent.classList.contains('siteHeaderGlovalNavigation'),
  157. target => target.id === 'siteHeaderLeftMenu',
  158. () => document.getElementById('siteHeaderLeftMenu'),
  159. {
  160. isTargetParent: targetParentIdFirefox
  161. ? parent => parent.id === targetParentIdFirefox
  162. : parent => parent.localName === 'body',
  163. isTarget: isTargetFirefox || (target => target.id === 'siteHeader'),
  164. }
  165. );
  166.  
  167. /**
  168. * ページの種類別に、実行する関数を切り替える。
  169. */
  170. function prepare()
  171. {
  172. Gettext.setLocale(pageType === 'community'
  173. ? document.getElementById('siteHeaderNotification').dataset.nicoLocale.replace('_', '-')
  174. : document.documentElement.lang);
  175.  
  176. if (pageType.startsWith('info_')) {
  177. // 英語版、または台湾版のニコニコインフォなら
  178. // 生放送へのリンクを取得
  179. const itemLive = document.querySelector('#siteHeader [href*="://live.nicovideo.jp/"]').parentElement;
  180. // 生放送リンクの複製
  181. const item = itemLive.cloneNode(true);
  182. // リンク文字を変更
  183. item.getElementsByTagName('span')[0].textContent = _('静画');
  184. // アドレスを変更
  185. item.getElementsByTagName('a')[0].href = 'https://seiga.nicovideo.jp/';
  186. // ヘッダに静画へのリンクを追加
  187. itemLive.before(item);
  188. return;
  189. }
  190.  
  191. switch (pageType) {
  192. case 'videoSearch':
  193. // 動画キーワード
  194. startScript(
  195. addTagSearchTabAboveSearchBox,
  196. parent => parent.classList.contains('formSearch'),
  197. target => target.id === 'search_united_form',
  198. () => document.getElementById('search_united_form'),
  199. {
  200. isTargetParent: parent => parent.localName === 'body',
  201. isTarget: target => target.localName === 'section',
  202. }
  203. );
  204. break;
  205.  
  206. case 'mylist':
  207. // マイリスト
  208. startScript(
  209. addTagSearchTabAboveSearchBox,
  210. parent => parent.id === 'form_search',
  211. target => target.id === 'search_united_form',
  212. () => document.getElementById('search_united_form'),
  213. {
  214. isTargetParent: parent => parent.id === 'PAGEMAIN',
  215. isTarget: target => target.id === 'PAGEBODY',
  216. }
  217. );
  218. break;
  219.  
  220. case 'top':
  221. // トップページ
  222. addTagSearchButtonToTopPage();
  223. break;
  224.  
  225. case 'imageSearch':
  226. // 静画キーワード
  227. startScript(
  228. addTagSearchTabAboveSearchBox,
  229. parent => parent.id === 'usearch_form',
  230. target => target.id === 'usearch_form_input',
  231. () => document.getElementById('usearch_form_input'),
  232. {
  233. isTargetParent: parent => parent.id === 'wrapper',
  234. isTarget: target => target.id === 'main',
  235. }
  236. );
  237. break;
  238.  
  239. case 'image':
  240. // 静画
  241. startScript(
  242. careteTabsBarToSearchBox,
  243. parent => parent.id === 'head_search_form',
  244. target => target.id === 'search_button',
  245. () => document.getElementById('search_button'),
  246. {
  247. isTargetParent: parent => parent.id === 'header_block',
  248. isTarget: () => true,
  249. }
  250. );
  251. break;
  252.  
  253. case 'liveSearch':
  254. // 生放送キーワード
  255. startScript(
  256. addTagSearchTabAboveSearchBox,
  257. parent => parent.classList.contains('search-input-area'),
  258. target => target.classList.contains('search-form'),
  259. () => document.getElementsByClassName('search-form')[0]
  260. );
  261. break;
  262.  
  263. case 'live':
  264. // 生放送
  265. startScript(
  266. careteTabsBarToSearchBox,
  267. parent => parent.classList.contains('search_program'),
  268. target => target.classList.contains('search_word'),
  269. () => document.getElementsByClassName('search_word')[0],
  270. {
  271. isTargetParent: parent => parent.localName === 'body',
  272. isTarget: target => target.id === 'page_header',
  273. });
  274. break;
  275.  
  276. case 'tag':
  277. if (document.doctype.publicId) {
  278. // 公開マイリスト等
  279. startScript(
  280. addOtherServiceTabsAboveSearchBox,
  281. parent => parent.id === 'search_tab',
  282. target => target.id === 'target_m',
  283. () => document.getElementById('target_m'),
  284. {
  285. isTargetParent: parent => parent.id === 'PAGEMAIN',
  286. isTarget: target => target.id === 'PAGEBODY',
  287. }
  288. );
  289. } else {
  290. // 動画タグ
  291. startScript(
  292. addOtherServiceTabsAboveSearchBox,
  293. parent => parent.classList.contains('videoSearchOption'),
  294. target => target.classList.contains('optMylist'),
  295. () => document.getElementsByClassName('optMylist')[0],
  296. {
  297. isTargetParent: parent => parent.localName === 'body',
  298. isTarget: target => target.localName === 'header',
  299. }
  300. );
  301. }
  302. break;
  303.  
  304. case 'user':
  305. // ユーザー
  306. startScript(
  307. addImageLinkToUserPageMenu,
  308. parent => parent.localName === 'body',
  309. target => target.classList.contains('optionOuter'),
  310. () => document.getElementsByClassName('optionOuter')[0]
  311. );
  312. break;
  313. }
  314. }
  315.  
  316.  
  317.  
  318. /**
  319. * 各サービスのキーワード検索ページの検索窓に、動画の「タグ」検索タブを追加する。
  320. * また、静画検索タブのスキーマをhttpsに変更する。
  321. */
  322. function addTagSearchTabAboveSearchBox()
  323. {
  324. // マイリスト検索タブの取得
  325. const mylistTab = document.querySelector('.tab_table td:nth-of-type(2), #search_frm_a a:nth-of-type(2), .search_tab_list li:nth-of-type(2), .seachFormA a:nth-of-type(2), li:nth-of-type(2).search-tab-item');
  326.  
  327. // マイリスト検索タブの複製
  328. const tagTab = mylistTab.cloneNode(true);
  329.  
  330. // タブ名を変更
  331. const anchor = tagTab.tagName.toLowerCase() === 'a' ? tagTab : tagTab.getElementsByTagName('a')[0];
  332. let tabNameNode = anchor.getElementsByTagName('div');
  333. tabNameNode = (tabNameNode.length > 0 ? tabNameNode[0].firstChild : anchor.firstChild);
  334. tabNameNode.data = _('タグ') + (pageType === 'liveSearch' ? '' : ' ( ');
  335.  
  336. // クラス名を変更・動画件数をリセット
  337. const searchCount = tagTab.querySelector('strong, span');
  338. switch (pageType) {
  339. case 'videoSearch':
  340. searchCount.classList.remove('more');
  341. break;
  342. case 'mylist':
  343. searchCount.style.removeProperty('color');
  344. break;
  345. case 'imageSearch':
  346. searchCount.classList.remove('search_value_em');
  347. searchCount.classList.add('search_value');
  348. break;
  349. }
  350. searchCount.textContent = '-';
  351.  
  352. if (searchCount.id) {
  353. // 生放送
  354. searchCount.id = 'search_count_tag';
  355. }
  356.  
  357. // 検索語句を取得
  358. const searchWordsPattern = /(?:\/(?:search|tag|mylist_search)\/|[?&]keyword=)([^?&#]+)/g;
  359. const result = location.href.match(searchWordsPattern);
  360. const searchWords
  361. = result ? searchWordsPattern.exec(result[pageType === 'liveSearch' ? result.length - 1 : 0])[1] : '';
  362.  
  363. // タグが付いた動画件数を取得・表示
  364. if (searchWords && location.host !== 'www.live.nicovideo.jp') {
  365. GM.xmlHttpRequest({
  366. method: 'GET',
  367. url: 'https://www.nicovideo.jp/tag/' + searchWords,
  368. onload: function (response) {
  369. const responseDocument = new DOMParser().parseFromString(response.responseText, 'text/html');
  370. const total = responseDocument.querySelector('.tagCaption .dataValue .num').textContent;
  371.  
  372. const trimmedThousandsSep = total.replace(/,/g, '');
  373. if (trimmedThousandsSep >= 100) {
  374. // 動画件数が100件を超えていれば
  375. switch (pageType) {
  376. case 'videoSearch':
  377. searchCount.classList.add('more');
  378. break;
  379. case 'mylist':
  380. searchCount.style.color = '#CC0000';
  381. break;
  382. case 'imageSearch':
  383. searchCount.classList.remove('search_value');
  384. searchCount.classList.add('search_value_em');
  385. break;
  386. case 'liveSearch':
  387. searchCount.classList.add('strong');
  388. break;
  389. }
  390. }
  391.  
  392. switch (pageType) {
  393. case 'mylist':
  394. searchCount.textContent = ' ' + total + ' ';
  395. break;
  396. case 'videoSearch':
  397. case 'imageSearch':
  398. searchCount.textContent = total;
  399. break;
  400. case 'liveSearch':
  401. searchCount.textContent = trimmedThousandsSep;
  402. break;
  403. }
  404. },
  405. });
  406. }
  407.  
  408. // 非アクティブタブを取得
  409. const inactiveTab = document.querySelector('.tab_0, .tab1, .search_tab_list a:not(.active), .search-tab-anchor');
  410.  
  411. // クラス名を変更
  412. anchor.className = inactiveTab.className;
  413.  
  414. // アドレスを変更
  415. anchor.href = 'https://www.nicovideo.jp/tag/' + searchWords + inactiveTab.search;
  416.  
  417. // タグ検索タブを追加
  418. mylistTab.parentNode.insertBefore(tagTab, mylistTab);
  419. if (pageType === 'liveSearch') {
  420. mylistTab.parentNode.insertBefore(new Text(' '), mylistTab);
  421. } else if (inactiveTab.classList.contains('tab1')) {
  422. // GINZAバージョン
  423. mylistTab.parentNode.insertBefore(tagTab.previousSibling.cloneNode(true), mylistTab);
  424. }
  425.  
  426. // 静画検索タブのスキーマをhttpsに変更
  427. for (const anchor of mylistTab.parentNode.querySelectorAll('[href^="http://"]')) {
  428. anchor.protocol = 'https';
  429. }
  430. }
  431.  
  432.  
  433.  
  434. /**
  435. * ニコニコ動画の上部に表示されている検索窓に、「静画」「生放送」を検索するタブを追加する。
  436. */
  437. function addOtherServiceTabsAboveSearchBox()
  438. {
  439. // スタイルの設定
  440. document.head.insertAdjacentHTML('beforeend', `<style>
  441. :root {
  442. --max-search-box-width: 268px;
  443. }
  444. #PAGEHEADER > div {
  445. display: flex;
  446. }
  447. #head_search {
  448. max-width: var(--max-search-box-width);
  449. flex-grow: 1;
  450. }
  451. #search_input {
  452. width: 100%;
  453. display: flex;
  454. }
  455. #search_input .typeText {
  456. flex-grow: 1;
  457. }
  458. #head_ads {
  459. margin-right: -26px;
  460. }
  461. #search_input #bar_search {
  462. box-sizing: border-box;
  463. width: 100% !important;
  464. }
  465. /*====================================
  466. GINZAバージョン
  467. */
  468. .siteHeader > .inner {
  469. display: flex;
  470. }
  471. .videoSearch {
  472. max-width: var(--max-search-box-width);
  473. flex-grow: 1;
  474. padding-left: 4px;
  475. padding-right: 4px;
  476. }
  477. .videoSearchOption {
  478. display: flex;
  479. white-space: nowrap;
  480. }
  481. .videoSearch form {
  482. display: flex;
  483. }
  484. .videoSearch form .inputText {
  485. flex-grow: 1;
  486. }
  487. /*------------------------------------
  488. ×ボタン
  489. */
  490. .clear-button-inner-tag {
  491. left: initial;
  492. right: 3px;
  493. }
  494. </style>`);
  495.  
  496. // タブリストの取得
  497. const mylistTab = document.querySelector('#target_t, .optMylist');
  498.  
  499. // タブの複製・追加
  500. mylistTab.parentElement.append(...[
  501. {
  502. type: 'image',
  503. title: _('静画を検索'),
  504. url: 'https://seiga.nicovideo.jp/search',
  505. text: _('静画'),
  506. },
  507. {
  508. type: 'live',
  509. title: _('番組を探す'),
  510. url: 'https://live.nicovideo.jp/search',
  511. text: _('生放送'),
  512. },
  513. ].map(function (option) {
  514. const tab = mylistTab.cloneNode(true);
  515. if (mylistTab.classList.contains('optMylist')) {
  516. // GINZAバージョン
  517. tab.classList.remove('optMylist');
  518. tab.classList.add('opt' + option.type[0].toUpperCase() + option.type.slice(1));
  519. tab.dataset.type = option.type;
  520. tab.getElementsByTagName('a')[0].textContent = option.text;
  521. } else {
  522. // 公開マイリスト等
  523. tab.id = 'target_' + option.type[0];
  524. tab.title = option.title;
  525. tab.setAttribute('onclick', tab.getAttribute('onclick').replace(/'.+?'/, '\'' + option.url + '\''));
  526. tab.textContent = option.text;
  527. }
  528. return tab;
  529. }));
  530.  
  531. GreasemonkeyUtils.executeOnUnsafeContext(/* global Nico */ function () {
  532. eval('Nico.Navigation.HeaderSearch.Controller.search = '
  533. + Nico.Navigation.HeaderSearch.Controller.search.toString().replace(/(switch.+?{[^}]+)/, `$1;
  534. break;
  535. case "image":
  536. d = "https://seiga.nicovideo.jp/search/" + e;
  537. break;
  538. case "live":
  539. d = "https://live.nicovideo.jp/search/" + e;
  540. break;
  541. `));
  542. });
  543. }
  544.  
  545.  
  546.  
  547. /**
  548. * 静画・生放送の上部に表示されている検索窓に、「動画キーワード」「動画タグ」「マイリスト」「静画」「生放送」を検索するタブバーを設置する。
  549. */
  550. function careteTabsBarToSearchBox()
  551. {
  552. // スタイルの設定
  553. document.head.insertAdjacentHTML('beforeend', `<style>
  554. #sg_search_box {
  555. /* 静画 */
  556. margin-top: 0.2em;
  557. }
  558. #live_header div.score_search { /* 生放送マイページ向けに詳細度を大きくしている */
  559. /* 生放送 */
  560. top: initial;
  561. }
  562. /*------------------------------------
  563. タブバー
  564. */
  565. [action$="search"] > ul {
  566. display: flex;
  567. /* 生放送 */
  568. font-size: 12px;
  569. }
  570. /* 静画 */
  571. #head_search_form > ul {
  572. margin-left: 1.3em;
  573. /* マンガ・電子書籍 */
  574. line-height: 1.4em;
  575. }
  576. #head_search_form > ul:hover ~ .search_form_text {
  577. border-color: #999;
  578. }
  579. /*------------------------------------
  580. タブ
  581. */
  582. [action$="search"] > ul > li {
  583. margin-left: 0.2em;
  584. white-space: nowrap;
  585. }
  586. [action$="search"] > ul > li > a {
  587. background: lightgrey;
  588. padding: 0.2em 0.3em 0.1em;
  589. color: inherit;
  590. /* 生放送 */
  591. text-decoration: none;
  592. }
  593. #head_search_form > ul > li > a:hover {
  594. /* 静画 */
  595. text-decoration: none;
  596. }
  597. /*------------------------------------
  598. 選択中のタブ
  599. */
  600. [action$="search"] > ul > li.current > a {
  601. color: white;
  602. background: dimgray;
  603. }
  604. </style>`);
  605.  
  606. /**
  607. * 静画検索のtargetパラメータの値。
  608. * @type {string}
  609. */
  610. let imageSearchParamValue = 'illust';
  611.  
  612. const form = document.querySelector('[action$="search"]');
  613. const textField = form[pageType === 'image' ? 'q' : 'keyword'];
  614.  
  615. if (pageType === 'image') {
  616. // 静画の場合
  617. const pathnameParts = document.querySelector('#logo > h1 > a').pathname.split('/');
  618. switch (pathnameParts[1]) {
  619. case 'manga':
  620. imageSearchParamValue = 'manga';
  621. break;
  622. case 'book':
  623. imageSearchParamValue = pathnameParts[2] === 'r18' ? 'book_r18' : 'book';
  624. break;
  625. }
  626. }
  627.  
  628. form.insertAdjacentHTML('afterbegin', `<ul>
  629. <li>
  630. <a href="https://www.nicovideo.jp/search/" title="${h(_('動画をキーワードで検索'))}">${h(_('キーワード'))}</a>
  631. </li>
  632. <li>
  633. <a href="https://www.nicovideo.jp/tag/" title="${h(_('動画をタグで検索'))}">${h(_('タグ'))}</a>
  634. </li>
  635. <li>
  636. <a href="https://www.nicovideo.jp/mylist_search/" title="${h(_('マイリストを検索'))}">${h(_('マイリスト'))}</a>
  637. </li>
  638. <li${pageType === 'image' ? ' class="current"' : ''}>
  639. <a href="https://seiga.nicovideo.jp/search/?target=${imageSearchParamValue}"
  640. title="${h(textField.defaultValue)}">${h(_('静画'))}</a>
  641. </li>
  642. <li${pageType === 'live' ? ' class="current"' : ''}>
  643. <a href="https://live.nicovideo.jp/search/" title="' + h(_('番組を探す')) + '">${h(_('生放送'))}</a>
  644. </li>
  645. </ul>`);
  646.  
  647. const defaultCurrentTabAnchor = form.querySelector('.current a');
  648.  
  649. document.addEventListener('click', function (event) {
  650. if (event.button !== 2 && event.target.matches('[action$="search"] > ul > li > a')) {
  651. // タブが副ボタン以外でクリックされたとき
  652. let searchWord = textField.value.trim();
  653. if (pageType === 'image' && textField.value === textField.defaultValue) {
  654. // 静画の場合、検索窓の値が既定値と一致していれば空欄とみなす
  655. searchWord = '';
  656. }
  657. if (searchWord) {
  658. // 検索語句が入力されていれば
  659. switchTab(event.target);
  660. event.target.pathname = event.target.pathname.replace(/[^/]*$/, encodeURIComponent(searchWord));
  661. setTimeout(function () {
  662. // リンク先を新しいタブで開いたとき
  663. switchTab(defaultCurrentTabAnchor);
  664. }, CURRENT_TAB_RESTORATION_DELAY);
  665. } else {
  666. // 検索語句が未入力なら
  667. event.preventDefault();
  668. if (event.button === 0) {
  669. // 主ボタンでクリックされていれば
  670. switchTab(event.target);
  671. }
  672. }
  673. }
  674. });
  675.  
  676. // TabSubmitをインストールしているとマウスボタンを取得できず、中クリック時にも同じタブで検索してしまうため分割
  677. form.addEventListener('click', function (event) {
  678. if (event.target.type === (pageType === 'image' ? 'image' : 'submit')) {
  679. // 送信ボタンをクリックしたとき
  680. const searchWord = textField.value !== textField.defaultValue && textField.value.trim();
  681. if (searchWord) {
  682. event.stopPropagation();
  683. event.preventDefault();
  684. const anchor = form.querySelector('.current a');
  685. anchor.pathname = anchor.pathname.replace(/[^/]*$/, encodeURIComponent(searchWord));
  686. location.assign(anchor.href);
  687. }
  688. }
  689. }, true);
  690.  
  691. addEventListener('pageshow', function (event) {
  692. if (event.persisted) {
  693. // 履歴にキャッシュされたページを再表示したとき
  694. switchTab(defaultCurrentTabAnchor);
  695. }
  696. });
  697.  
  698. /**
  699. * 選択しているタブを切り替える。
  700. * @param {HTMLAnchorElement} target - 切り替え先のタブのリンク。
  701. */
  702. function switchTab(target) {
  703. form.getElementsByClassName('current')[0].classList.remove('current');
  704. target.parentElement.classList.add('current');
  705. if (pageType === 'image') {
  706. // 静画
  707. if (textField.defaultValue === textField.value) {
  708. // 検索語句が未入力なら
  709. textField.defaultValue = textField.value = target.title;
  710. } else {
  711. // 検索語句が入力されていれば
  712. textField.defaultValue = target.title;
  713. }
  714. } else {
  715. // 生放送
  716. textField.placeholder = target.title;
  717. }
  718. }
  719. }
  720.  
  721.  
  722.  
  723. /**
  724. * 総合トップページの検索窓に、動画「タブ」「マイリスト」検索ボタンを追加する。
  725. */
  726. function addTagSearchButtonToTopPage()
  727. {
  728. // スタイルの設定
  729. document.head.insertAdjacentHTML('beforeend', `<style>
  730. .CrossSearch {
  731. display: flex;
  732. margin-right: 1em;
  733. }
  734. .CrossSearch-services {
  735. display: flex;
  736. }
  737. .CrossSearch-service {
  738. width: unset;
  739. padding: 0 0.5em;
  740. white-space: nowrap;
  741. }
  742. .CrossSearch-form {
  743. width: unset;
  744. }
  745. </style>`);
  746.  
  747. // 静画検索ボタンの取得
  748. const refItem = document.querySelector('.CrossSearch-service[data-service="seiga"]');
  749. refItem.dataset.baseUrl = refItem.dataset.baseUrl.replace('http://', 'https://');
  750.  
  751. const tagItem = refItem.cloneNode(true);
  752. tagItem.textContent = _('タグ');
  753. tagItem.dataset.service = 'tag';
  754. tagItem.dataset.baseUrl = 'https://www.nicovideo.jp/tag/';
  755. refItem.before(tagItem);
  756.  
  757. const mylist = refItem.cloneNode(true);
  758. mylist.textContent = _('マイリスト');
  759. mylist.dataset.service = 'mylist';
  760. mylist.dataset.baseUrl = 'https://www.nicovideo.jp/mylist_search/';
  761. refItem.before(mylist);
  762. }
  763.  
  764.  
  765.  
  766. /**
  767. * ユーザーページ左側のメニューに、静画へのリンクを追加する。
  768. */
  769. function addImageLinkToUserPageMenu()
  770. {
  771. // スタイルの設定
  772. document.head.insertAdjacentHTML('beforeend', `<style>
  773. .sidebar ul li.imageTab a span {
  774. width: 22px;
  775. height: 20px;
  776. background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAUCAQAAAAjdQW7AAAA/ElEQVQoz9WRzU3EMBCFv3EibRN2CyDtLiJJIRSSQlIIhSQgscDWYDeBhGc4ZJf87YErzwdr5M9vnmbEmCvz2FudMQSG12a3eBWbgcdOKgt4wwBJxM/mJnzorNKAX3Y6y7wqL+iz1uZ1ATocS5UjmmtbeQqSXGT1nX2Xa/WKzQ5IcsNbs3LOWDW5yu/t4umJdYxjr0EnJElUNIjXxEal1mPbMVPBqYG7aOC/2K1gl9FZUuXQ3/dKRsNDt3G2xf7M6zW/F8+t0V1l5KlIbFKXJRldZ0OSG97bDVwMBPF5WgWSJPJybrfTEGPffft8mYhQQPpoC25JjL/L8f/gH1eMbYCeUydgAAAAAElFTkSuQmCC");
  777. }
  778. </style>`);
  779.  
  780. const nextItem = document.getElementsByClassName('stampTab')[0];
  781.  
  782. const item = nextItem.cloneNode(true);
  783. const classList = item.classList;
  784. classList.remove('stampTab', 'active');
  785. classList.add('imageTab');
  786. const anchor = item.getElementsByTagName('a')[0];
  787. anchor.href = 'https://seiga.nicovideo.jp/user/illust/' + /[0-9]+/.exec(anchor.pathname)[0];
  788. anchor.lastChild.data = _('静画');
  789.  
  790. nextItem.prepend(item);
  791. }