Append Tag Searching Tub

『niconico』の検索窓にタグ検索タブを追加 / Adds "tag" search tab above search box in "niconico"

当前为 2014-04-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Append Tag Searching Tub
  3. // @namespace http://loda.jp/script/
  4. // @id niconico-adds-search-tab-347021
  5. // @version 3.0.1
  6. // @description 『niconico』の検索窓にタグ検索タブを追加 / Adds "tag" search tab above search box in "niconico"
  7. // @match http://www.nicovideo.jp/*
  8. // @match http://seiga.nicovideo.jp/search/*
  9. // @match http://live.nicovideo.jp/*
  10. // @match http://watch.live.nicovideo.jp/*
  11. // @match http://com.nicovideo.jp/*
  12. // @match http://blog.nicovideo.jp/en_info/*
  13. // @match http://tw.blog.nicovideo.jp/*
  14. // @match http://info.nicovideo.jp/psvita/en/*
  15. // @grant GM_xmlhttpRequest
  16. // @domain www.nicovideo.jp
  17. // @domain seiga.nicovideo.jp
  18. // @domain live.nicovideo.jp
  19. // @domain watch.live.nicovideo.jp
  20. // @domain com.nicovideo.jp
  21. // @domain blog.nicovideo.jp
  22. // @domain tw.blog.nicovideo.jp
  23. // @domain info.nicovideo.jp
  24. // @run-at document-start
  25. // @icon data:image/vnd.microsoft.icon;base64,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
  26. // @author 100の人 https://userscripts.org/users/347021
  27. // @license Creative Commons Attribution 3.0 Unported License
  28. // ==/UserScript==
  29.  
  30. (function () {
  31. 'use strict';
  32.  
  33. polyfill();
  34.  
  35. // L10N
  36. setLocalizedTexts({
  37. 'en': {
  38. '静画を検索': 'Search Image',
  39. '静画': 'Images',
  40. '生放送を検索': 'Search Live Program',
  41. '生放送': 'Live',
  42. 'タグ': 'Tags',
  43. 'マンガ': 'Comics',
  44. },
  45. 'zh': {
  46. '静画を検索': '搜尋靜畫',
  47. '静画': '靜畫',
  48. '生放送を検索': '搜尋生放送',
  49. '生放送': '生放送',
  50. 'タグ': '標籤',
  51. 'マンガ': '漫畫',
  52. },
  53. });
  54.  
  55. /**
  56. * 検索窓の最大幅
  57. * @constant {string}
  58. */
  59. var MAX_SEARCH_BOX_WIDTH = '268px';
  60.  
  61.  
  62.  
  63. var host = window.location.host, pathname = window.location.pathname, pageType,
  64. targetParentIdFirefox, isTargetParentFirefox, isTargetFirefox;
  65.  
  66. // 検索ページの種類を取得
  67. switch (host) {
  68. case 'www.nicovideo.jp':
  69. if (window.location.pathname === '/') {
  70. pageType = 'top';
  71. } else if (/^\/(?:search\/|mylist_search(?:\/|$))/.test(pathname)) {
  72. pageType = 'video';
  73. } else if (/^\/(?:(?:tag|related_tag|watch|mylist)\/|(?:recent|newarrival|hotlist|video_top|openlist|playlist|recommendations)(?:\/|$))/.test(pathname)) {
  74. pageType = 'tag';
  75. }
  76. break;
  77. case 'seiga.nicovideo.jp':
  78. pageType = 'image';
  79. break;
  80. case 'live.nicovideo.jp':
  81. case 'watch.live.nicovideo.jp':
  82. if (pathname.startsWith('/search')) {
  83. pageType = 'live';
  84. }
  85. break;
  86. case 'info.nicovideo.jp':
  87. if (pathname.startsWith('/psvita/en/')) {
  88. // 英語版PS Vita紹介ページ
  89. startScript(prepare,
  90. function (parent) { return parent.localName === 'body'; },
  91. function (target) { return target.id === 'header'; },
  92. function () { return document.getElementById('header'); },
  93. {
  94. isTargetParent: function (parent) { return parent.localName === 'html'; },
  95. isTarget: function (target) { return target.localName === 'body'; },
  96. });
  97. }
  98. return;
  99. }
  100.  
  101. // 上部メニューが追加されるまで待機
  102. switch (host) {
  103. case 'seiga.nicovideo.jp':
  104. // 静画
  105. isTargetFirefox = function (target) { return target.id === 'wrapper'; };
  106. break;
  107. case 'live.nicovideo.jp':
  108. // 生放送
  109. targetParentIdFirefox = 'body_header';
  110. break;
  111. case 'blog.nicovideo.jp':
  112. // 英語版ニコニコインフォ
  113. targetParentIdFirefox = 'container-inner';
  114. break;
  115. case 'tw.blog.nicovideo.jp':
  116. // 台湾版ニコニコインフォ
  117. targetParentIdFirefox = 'header';
  118. break;
  119. case 'info.nicovideo.jp':
  120. break;
  121. }
  122. if (!isTargetParentFirefox) {
  123. if (targetParentIdFirefox) {
  124. isTargetParentFirefox = function (parent) { return parent.id === targetParentIdFirefox; };
  125. } else {
  126. isTargetParentFirefox = function (parent) { return parent.localName === 'body'; };
  127. }
  128. }
  129. startScript(prepare,
  130. function (parent) { return parent.classList.contains('siteHeaderGlovalNavigation'); },
  131. function (target) { return target.id === 'siteHeaderLeftMenu'; },
  132. function () { return document.getElementById('siteHeaderLeftMenu'); },
  133. {
  134. isTargetParent: isTargetParentFirefox,
  135. isTarget: isTargetFirefox || function (target) { return target.id === 'siteHeader'; },
  136. });
  137.  
  138. function prepare () {
  139. var parentId, parentIdFirefox, targetId, targetIdFirefox, isTargetParent, isTargetParentFirefox,
  140. textVideo, harajuku, itemLive, item;
  141.  
  142. // ニコニコ生放送ではlang属性値が常にja-JPのため、ニコニコ動画へのリンク文字によって、ページの言語を判定する
  143. textVideo = document.querySelector('[href^="http://www.nicovideo.jp/video_top"]').textContent;
  144. if (textVideo.contains('Video')) {
  145. setlang('en');
  146. } else if (textVideo.contains('動畫')) {
  147. setlang('zh');
  148. }
  149. if (!document.querySelector(pageType === 'image' ? '#siteHeader [href="/?header"], #siteHeader [href="/"]' : '#siteHeader [href^="http://seiga.nicovideo.jp/"], #globalNav [href^="http://seiga.nicovideo.jp/"]')) {
  150. // ヘッダに静画へのリンクが無ければ
  151. // 生放送へのリンクを取得
  152. itemLive = document.querySelector('#siteHeader [href^="http://live.nicovideo.jp/"], #globalNav [href^="http://live.nicovideo.jp/"]').parentNode;
  153. // 生放送リンクの複製
  154. item = itemLive.cloneNode(true);
  155. // リンク文字を変更
  156. (item.getElementsByTagName('span')[0] || item.getElementsByTagName('a')[0]).textContent = _('静画');
  157. // アドレスを変更
  158. item.getElementsByTagName('a')[0].host = 'seiga.nicovideo.jp';
  159. // ヘッダに静画へのリンクを追加
  160. itemLive.parentNode.insertBefore(item, itemLive);
  161. }
  162. // スクリプトを起動
  163. if (!pageType) {
  164. return;
  165. }
  166. harajuku = document.doctype.publicId;
  167. switch (pageType) {
  168. case 'video':
  169. if (harajuku) {
  170. // マイリスト検索、キーワード検索
  171. parentId = 'form_search';
  172. targetId = 'search_united_form';
  173. parentIdFirefox = 'PAGEMAIN';
  174. targetIdFirefox = 'PAGEBODY';
  175. } else {
  176. // GINZAバージョンのキーワード検索
  177. startScript(main,
  178. function (parent) { return parent.classList.contains('formSearch'); },
  179. function (target) { return target.id === 'search_united_form'; },
  180. function () { return document.getElementById('search_united_form'); },
  181. {
  182. isTargetParent: function (parent) { return parent.localName === 'body'; },
  183. isTarget: function (target) { return target.localName === 'section'; },
  184. });
  185. return;
  186. }
  187. break;
  188. case 'top':
  189. // トップページ
  190. main = mainTop;
  191. parentId = 'searchFormInner';
  192. targetId = 'searchForm';
  193. isTargetParentFirefox = function (parent) {
  194. return parent.id === 'main_container' || parent.localName === 'body';
  195. };
  196. targetIdFirefox = 'searchFormWrap';
  197. break;
  198. case 'image':
  199. // 静画
  200. parentId = 'usearch_form';
  201. targetId = 'usearch_form_input';
  202. parentIdFirefox = 'wrapper';
  203. targetIdFirefox = 'main';
  204. break;
  205. case 'live':
  206. // 生放送
  207. isTargetParentFirefox = isTargetParent = function (target) {
  208. return target.classList.contains('container');
  209. };
  210. targetIdFirefox = targetId = 'form_frm_btm';
  211. break;
  212. case 'tag':
  213. if (harajuku) {
  214. // タグ検索等
  215. main = mainTag;
  216. parentId = 'search_tab';
  217. targetId = 'target_m';
  218. parentIdFirefox = 'PAGEMAIN';
  219. targetIdFirefox = 'PAGEBODY';
  220. } else {
  221. // GINZAバージョンのタグ検索
  222. startScript(mainTag,
  223. function (parent) { return parent.classList.contains('videoSearchOption'); },
  224. function (target) { return target.classList.contains('optMylist'); },
  225. function () { return document.getElementsByClassName('optMylist')[0]; },
  226. {
  227. isTargetParent: function (parent) { return parent.localName === 'body'; },
  228. isTarget: function (target) { return target.localName === 'header'; },
  229. });
  230. return;
  231. }
  232. }
  233. startScript(main,
  234. isTargetParent || function (parent) { return parent.id === parentId; },
  235. function (target) { return target.id === targetId; },
  236. function () { return document.getElementById(targetId); },
  237. {
  238. isTargetParent: isTargetParentFirefox || function (parent) { return parent.id === parentIdFirefox; },
  239. isTarget: function (target) { return target.id === targetIdFirefox; },
  240. });
  241. }
  242.  
  243.  
  244.  
  245. // タグ検索
  246. function mainTag () {
  247. var mylistTab, tabList, styleSheet, cssRules, script;
  248. // スタイルの設定
  249. styleSheet = document.head.appendChild(document.createElement('style')).sheet;
  250. cssRules = styleSheet.cssRules;
  251. [
  252. '#PAGEHEADER > div {'
  253. + 'display: flex;'
  254. + '}',
  255. '#head_search {'
  256. + 'max-width: ' + MAX_SEARCH_BOX_WIDTH + ';'
  257. + 'flex-grow: 1;'
  258. + '}',
  259. '#search_input {'
  260. + 'width: 100%;'
  261. + 'display: flex;'
  262. + '}',
  263. '#search_input .typeText {'
  264. + 'flex-grow: 1;'
  265. + '}',
  266. '#head_ads {'
  267. + 'margin-right: -26px;'
  268. + '}',
  269. '#search_input #bar_search {'
  270. + '-moz-box-sizing: border-box;'
  271. + 'box-sizing: border-box;'
  272. + 'width: 100% !important;'
  273. + '}',
  274. // GINZAバージョン
  275. '.siteHeader > .inner {'
  276. + 'display: flex;'
  277. + '}',
  278. '.videoSearch {'
  279. + 'max-width: ' + MAX_SEARCH_BOX_WIDTH + ';'
  280. + 'flex-grow: 1;'
  281. + 'padding-left: 4px;'
  282. + 'padding-right: 4px;'
  283. + '}',
  284. '.videoSearch form {'
  285. + 'display: flex;'
  286. + '}',
  287. '.videoSearch form .inputText {'
  288. + 'flex-grow: 1;'
  289. + '}',
  290. ].forEach(function (rule) {
  291. styleSheet.insertRule(rule, cssRules.length);
  292. });
  293. // タブリストの取得
  294. mylistTab = document.querySelector('#target_m, .optMylist');
  295. tabList = mylistTab.parentNode;
  296. // タブの複製・追加
  297. [
  298. {
  299. type: 'image',
  300. title: _('静画を検索'),
  301. uri: 'http://seiga.nicovideo.jp/search',
  302. text: _('静画'),
  303. },
  304. {
  305. type: 'live',
  306. title: _('生放送を検索'),
  307. uri: 'http://live.nicovideo.jp/search',
  308. text: _('生放送'),
  309. },
  310. ].forEach(function (option) {
  311. var tab = mylistTab.cloneNode(true);
  312. if (mylistTab.classList.contains('optMylist')) {
  313. // GINZAバージョン
  314. tab.classList.remove('optMylist');
  315. tab.classList.add('opt' + option.type[0].toUpperCase() + option.type.slice(1));
  316. tab.dataset.type = option.type;
  317. tab.getElementsByTagName('a')[0].textContent = option.text;
  318. } else {
  319. // 原宿バージョン
  320. tab.id = 'target_' + option.type[0];
  321. tab.title = option.title;
  322. tab.setAttribute('onclick', tab.getAttribute('onclick').replace(/'.+?'/, '\'' + option.uri + '\''));
  323. tab.textContent = option.text;
  324. }
  325. tabList.appendChild(tab);
  326. });
  327. if (mylistTab.classList.contains('optMylist')) {
  328. // GINZAバージョン
  329. script = document.createElement('script');
  330. script.text = '(' + (function () {
  331. eval('Nico.Navigation.HeaderSearch.Controller.search = ' + Nico.Navigation.HeaderSearch.Controller.search.toString().replace(/(switch.+?{.+?)(})/, '$1; break;'
  332. + 'case "image":'
  333. + 'd = "http://seiga.nicovideo.jp/search/" + e; break;'
  334. + 'case "live":'
  335. + 'd = "http://live.nicovideo.jp/search/" + e; break;'
  336. + '$2'));
  337. }).toString() + ')();';
  338. document.head.appendChild(script);
  339. }
  340. }
  341.  
  342.  
  343.  
  344. // トップページ
  345. function mainTop() {
  346. var styleSheet, cssRules, refItem, item, anchor;
  347. fixPrototypeJavaScriptFramework();
  348. // スタイルの設定
  349. styleSheet = document.head.appendChild(document.createElement('style')).sheet;
  350. cssRules = styleSheet.cssRules;
  351. [
  352. '#searchFormInner {'
  353. + 'width: auto;'
  354. + 'margin-left: 136px;'
  355. + '}',
  356. ].forEach(function (rule) {
  357. styleSheet.insertRule(rule, cssRules.length);
  358. });
  359. // マイリスト検索ボタンの取得
  360. refItem = document.getElementsByClassName('sMylist')[0].parentNode;
  361. // マイリスト検索ボタンの複製
  362. item = refItem.cloneNode(true);
  363. // ボタン名を変更
  364. anchor = item.getElementsByTagName('a')[0];
  365. anchor.textContent = _('タグ');
  366. // クラス名を変更
  367. anchor.className = 'sVideo';
  368. // アドレスを変更
  369. anchor.href = 'http://www.nicovideo.jp/tag/';
  370. // タグ検索ボタンを追加
  371. refItem.parentNode.insertBefore(item, refItem);
  372. if (!document.getElementsByClassName('sSeiga')[0]) {
  373. // 静画検索ボタンが存在しなければ
  374. // 生放送検索の取得
  375. refItem = document.getElementsByClassName('sLive')[0].parentNode;
  376. // 生放送検索の複製
  377. item = refItem.cloneNode(true);
  378. // ボタン名を変更
  379. anchor = item.getElementsByTagName('a')[0];
  380. anchor.textContent = _('静画');
  381. // クラス名を変更
  382. anchor.className = 'sSeiga';
  383. // アドレスを変更
  384. anchor.href = 'http://seiga.nicovideo.jp/search/';
  385. // 静画検索を追加
  386. refItem.parentNode.insertBefore(item, refItem);
  387. startScript(function () {
  388. var list, item, anchor;
  389. // メニューの生放送リンクの取得
  390. list = document.querySelector('.service_main .live').parentNode.parentNode;
  391. // 生放送リンクの複製
  392. item = list.cloneNode(true);
  393. // リンク文字を変更
  394. anchor = item.getElementsByTagName('a')[0];
  395. anchor.title = anchor.textContent = _('静画');
  396. // クラス名を変更
  397. anchor.classList.remove('live');
  398. anchor.classList.add('seiga');
  399. // アドレスを変更
  400. item.getElementsByTagName('a')[0].href = 'http://seiga.nicovideo.jp/';
  401. // メニューに静画へのリンクを追加
  402. list.parentNode.insertBefore(item, list);
  403.  
  404. // サブメニューの複製
  405. item = document.getElementsByClassName('service_sub')[0].cloneNode(true);
  406. // 2つ目以降の要素を削除
  407. Array.prototype.forEach.call(item.querySelectorAll('li:first-child ~ li'), function (item) {
  408. item.parentNode.removeChild(item);
  409. });
  410. // リンク文字を変更
  411. anchor = item.getElementsByTagName('a')[0];
  412. anchor.title = anchor.textContent = _('マンガ');
  413. // アドレスを変更
  414. item.getElementsByTagName('a')[0].href = 'http://seiga.nicovideo.jp/manga/';
  415. // メニューに静画のサブメニューへのリンクを追加
  416. list.parentNode.insertBefore(item, list);
  417. },
  418. function (parent) { return parent.id === 'sideNav'; },
  419. function (target) { return target.id === 'trendyTags'; },
  420. function () { return document.querySelector('#menuService [href="http://live.nicovideo.jp/timetable/"]'); },
  421. {
  422. isTarget: function (target) { return target.id === 'NewServiceList'; },
  423. });
  424. }
  425. }
  426.  
  427.  
  428.  
  429. // キーワード検索、マイリスト検索、静画検索、生放送検索
  430. function main() {
  431. var inactiveTab, mylistTab, tagTab, tabNameNode, searchCount, anchor, searchWords = '', searchWordsPattern;
  432. // マイリスト検索タブの取得
  433. mylistTab = document.querySelector('.tab_table td:nth-of-type(2), #search_frm_a a:nth-of-type(2), .seachFormA a:nth-of-type(2)');
  434. // マイリスト検索タブの複製
  435. tagTab = mylistTab.cloneNode(true);
  436. // タブ名を変更
  437. anchor = tagTab.tagName.toLowerCase() === 'a' ? tagTab : tagTab.getElementsByTagName('a')[0];
  438. tabNameNode = anchor.getElementsByTagName('div');
  439. tabNameNode = (tabNameNode.length > 0 ? tabNameNode[0].firstChild : anchor.firstChild);
  440. tabNameNode.data = _('タグ') + (pageType === 'live' ? '(' : ' ( ');
  441. // クラス名を変更・動画件数をリセット
  442. searchCount = tagTab.querySelector('strong, span');
  443. if (pageType === 'image') {
  444. searchCount.classList.remove('search_value_em');
  445. searchCount.classList.add('search_value');
  446. } else if (pageType === 'live') {
  447. searchCount.classList.remove('Redtxt');
  448. } else{
  449. searchCount.style.removeProperty('color');
  450. }
  451. searchCount.textContent = '-';
  452. if (searchCount.id) {
  453. // 生放送
  454. searchCount.id = 'search_count_tag';
  455. }
  456.  
  457. // 検索語句を取得
  458. searchWordsPattern = /(?:\/(?:search|tag|mylist_search)\/|[?&]keyword=)([^?&#]+)/g;
  459. if (searchWords = window.location.href.match(searchWordsPattern)) {
  460. searchWords = searchWordsPattern.exec(searchWords[pageType === 'live' ? searchWords.length - 1 : 0])[1];
  461. }
  462. // タグが付いた動画件数を取得・表示
  463. if (searchWords) {
  464. GM_xmlhttpRequest({
  465. method: 'GET',
  466. url: 'http://www.nicovideo.jp/tag/' + searchWords,
  467. onload: function (response) {
  468. var responseDocument, total, trimmedThousandsSep;
  469. responseDocument = new DOMParser().parseFromString(response.responseText, 'text/html');
  470. if (!responseDocument) {
  471. // Blink
  472. // Issue 265379: DOMParser + text/html does not work <http://code.google.com/p/chromium/issues/detail?id=265379>
  473. responseDocument = document.implementation.createHTMLDocument();
  474. responseDocument.documentElement.innerHTML = response.responseText;
  475. }
  476. total = responseDocument.title.contains('(原宿)')
  477. // 原宿バージョン
  478. ? /[,0-9]+/.exec(responseDocument.getElementsByClassName('searchTagTotal')[0].textContent)[0]
  479. // GINZAバージョン
  480. : responseDocument.querySelector('.tagCaption .dataValue .num').textContent;
  481. trimmedThousandsSep = total.replace(/,/g, '');
  482. if (trimmedThousandsSep >= 100) {
  483. if (pageType === 'image') {
  484. searchCount.classList.remove('search_value');
  485. searchCount.classList.add('search_value_em');
  486. } else if (pageType === 'live') {
  487. searchCount.classList.add('Redtxt');
  488. } else {
  489. searchCount.style.color = '#CC0000';
  490. }
  491. }
  492. searchCount.textContent = pageType === 'live' ? trimmedThousandsSep : (pageType === 'image' ? total : ' ' + total + ' ');
  493. }
  494. });
  495. }
  496. // 非アクティブタブを取得
  497. inactiveTab = document.querySelector('.tab_0, .tab1');
  498. // クラス名を変更
  499. anchor.className = inactiveTab.className;
  500. // アドレスを変更
  501. anchor.href = 'http://www.nicovideo.jp/tag/' + searchWords + inactiveTab.search;
  502. // タグ検索タブを追加
  503. mylistTab.parentNode.insertBefore(tagTab, mylistTab);
  504. if (inactiveTab.classList.contains('tab1')) {
  505. // GINZAバージョン
  506. mylistTab.parentNode.insertBefore(tagTab.previousSibling.cloneNode(true), mylistTab);
  507. }
  508. }
  509.  
  510.  
  511.  
  512. /**
  513. * 挿入された節の親節が、目印となる節の親節か否かを返すコールバック関数
  514. * @callback isTargetParent
  515. * @param {(Document|Element)} parent
  516. * @returns {boolean}
  517. */
  518.  
  519. /**
  520. * 挿入された節が、目印となる節か否かを返すコールバック関数
  521. * @callback isTarget
  522. * @param {(DocumentType|Element)} target
  523. * @returns {boolean}
  524. */
  525.  
  526. /**
  527. * 目印となる節が文書に存在するか否かを返すコールバック関数
  528. * @callback existsTarget
  529. * @returns {boolean}
  530. */
  531.  
  532. /**
  533. * 目印となる節が挿入された直後に関数を実行する
  534. * @param {Function} main - 実行する関数
  535. * @param {isTargetParent} isTargetParent
  536. * @param {isTarget} isTarget
  537. * @param {existsTarget} existsTarget
  538. * @param {Object} [callbacksForFirefox] - DOMContentLoaded前のタイミングで1回だけスクリプトを起動させる場合に設定
  539. * @param {isTargetParent} [callbacksForFirefox.isTargetParent] - FirefoxにおけるisTargetParent
  540. * @param {isTarget} [callbacksForFirefox.isTarget] - FirefoxにおけるisTarget
  541. * @version 2013-09-23
  542. */
  543. function startScript(main, isTargetParent, isTarget, existsTarget, callbacksForFirefox) {
  544. var observer, flag;
  545. // FirefoxのDOMContentLoaded前のMutationObserverは、要素をまとめて挿入したと見なすため、isTargetParent、isTargetを変更
  546. if (callbacksForFirefox && window.navigator.userAgent.contains(' Firefox/')) {
  547. if (callbacksForFirefox.isTargetParent) {
  548. isTargetParent = callbacksForFirefox.isTargetParent;
  549. }
  550. if (callbacksForFirefox.isTarget) {
  551. isTarget = callbacksForFirefox.isTarget;
  552. }
  553. }
  554. // 指定した節が既に存在していれば、即実行
  555. startMain();
  556. if (flag) {
  557. return;
  558. }
  559. observer = new MutationObserver(mutationCallback);
  560. observer.observe(document, {
  561. childList: true,
  562. subtree: true,
  563. });
  564. if (callbacksForFirefox) {
  565. // DOMContentLoadedまでにスクリプトを実行できなかった場合、監視を停止(指定した節が存在するか確認し、存在すれば実行)
  566. document.addEventListener('DOMContentLoaded', function stopScript(event) {
  567. event.target.removeEventListener('DOMContentLoaded', stopScript);
  568. if (observer) {
  569. observer.disconnect();
  570. }
  571. startMain();
  572. flag = true;
  573. });
  574. }
  575. /**
  576. * 目印となる節が挿入されたら、監視を停止し、{@link checkExistingTarget}を実行する
  577. * @param {MutationRecord[]} mutations - a list of MutationRecord objects
  578. * @param {MutationObserver} observer - the constructed MutationObserver object
  579. */
  580. function mutationCallback(mutations, observer) {
  581. var mutation, target, nodeType, addedNodes, addedNode, i, j, l, l2;
  582. for (i = 0, l = mutations.length; i < l; i++) {
  583. mutation = mutations[i];
  584. target = mutation.target;
  585. nodeType = target.nodeType;
  586. if ((nodeType === Node.ELEMENT_NODE || nodeType === Node.DOCUMENT_NODE) && isTargetParent(target)) {
  587. // 子が追加された節が要素節か文書節で、かつそのノードについてisTargetParentが真を返せば
  588. addedNodes = Array.prototype.slice.call(mutation.addedNodes);
  589. for (j = 0, l2 = addedNodes.length; j < l2; j++) {
  590. addedNode = addedNodes[j];
  591. nodeType = addedNode.nodeType;
  592. if ((nodeType === Node.ELEMENT_NODE || nodeType === Node.DOCUMENT_TYPE_NODE) && isTarget(addedNode)) {
  593. // 追加された子が要素節か文書型節で、かつそのノードについてisTargetが真を返せば
  594. observer.disconnect();
  595. checkExistingTarget(0);
  596. return;
  597. }
  598. }
  599. }
  600. }
  601. }
  602. /**
  603. * {@link startMain}を実行し、スクリプトが開始されていなければ再度実行
  604. * @param {number} count - {@link startMain}を実行した回数
  605. */
  606. function checkExistingTarget(count) {
  607. var LIMIT = 500, INTERVAL = 10;
  608. startMain();
  609. if (!flag && count < LIMIT) {
  610. window.setTimeout(checkExistingTarget, INTERVAL, count + 1);
  611. }
  612. }
  613. /**
  614. * 指定した節が存在するか確認し、存在すれば監視を停止しスクリプトを実行
  615. */
  616. function startMain() {
  617. if (!flag && existsTarget()) {
  618. flag = true;
  619. main();
  620. }
  621. }
  622. }
  623.  
  624. /**
  625. * prototype汚染が行われる Prototype JavaScript Framework (prototype.js) 1.5.1.1 のバグを修正(Tampermonkey用)
  626. */
  627. function fixPrototypeJavaScriptFramework() {
  628. [
  629. [document, 'getElementsByClassName'],
  630. ].forEach(function (objectProperty) {
  631. delete objectProperty[0][objectProperty[1]];
  632. });
  633. }
  634.  
  635. /**
  636. * 国際化・地域化関数の読み込み、ECMAScript仕様のPolyfill
  637. */
  638. function polyfill() {
  639. // i18n
  640. (function () {
  641. /**
  642. * 翻訳対象文字列 (msgid) の言語
  643. * @constant {string}
  644. */
  645. var ORIGINAL_LOCALE = 'ja';
  646. /**
  647. * クライアントの言語の翻訳リソースが存在しないとき、どの言語に翻訳するか
  648. * @constant {string}
  649. */
  650. var DEFAULT_LOCALE = 'en';
  651. /**
  652. * 以下のような形式の翻訳リソース
  653. * {
  654. * 'IETF言語タグ': {
  655. * '翻訳前 (msgid)': '翻訳後 (msgstr)',
  656. * ……
  657. * },
  658. * ……
  659. * }
  660. * @typedef {Object} LocalizedTexts
  661. */
  662. /**
  663. * クライアントの言語。{@link setlang}から変更される
  664. * @type {string}
  665. * @access private
  666. */
  667. var langtag = 'ja';
  668. /**
  669. * クライアントの言語のlanguage部分。{@link setlang}から変更される
  670. * @type {string}
  671. * @access private
  672. */
  673. var language = 'ja';
  674. /**
  675. * 翻訳リソース。{@link setLocalizedTexts}から変更される
  676. * @type {LocalizedTexts}
  677. * @access private
  678. */
  679. var multilingualLocalizedTexts = {};
  680. multilingualLocalizedTexts[ORIGINAL_LOCALE] = {};
  681. /**
  682. * テキストをクライアントの言語に変換する
  683. * @param {string} message - 翻訳前
  684. * @returns {string} 翻訳後
  685. */
  686. window._ = window.gettext = function (message) {
  687. // クライアントの言語の翻訳リソースが存在すれば、それを返す
  688. return langtag in multilingualLocalizedTexts && multilingualLocalizedTexts[langtag][message]
  689. // 地域下位タグを取り除いた言語タグの翻訳リソースが存在すれば、それを返す
  690. || language in multilingualLocalizedTexts && multilingualLocalizedTexts[language][message]
  691. // デフォルト言語の翻訳リソースが存在すれば、それを返す
  692. || DEFAULT_LOCALE in multilingualLocalizedTexts && multilingualLocalizedTexts[DEFAULT_LOCALE][message]
  693. // そのまま返す
  694. || message;
  695. };
  696. /**
  697. * {@link gettext}から参照されるクライアントの言語を設定する
  698. * @param {string} lang - IETF言語タグ(「language」と「language-REGION」にのみ対応)
  699. */
  700. window.setlang = function (lang) {
  701. lang = lang.split('-', 2);
  702. language = lang[0].toLowerCase();
  703. langtag = language + (lang[1] ? '-' + lang[1].toUpperCase() : '');
  704. };
  705. /**
  706. * {@link gettext}から参照される翻訳リソースを追加する
  707. * @param {LocalizedTexts} localizedTexts
  708. */
  709. window.setLocalizedTexts = function (localizedTexts) {
  710. var localizedText, lang, language, langtag, msgid;
  711. for (lang in localizedTexts) {
  712. localizedText = localizedTexts[lang];
  713. lang = lang.split('-');
  714. language = lang[0].toLowerCase();
  715. langtag = language + (lang[1] ? '-' + lang[1].toUpperCase() : '');
  716. if (langtag in multilingualLocalizedTexts) {
  717. // すでに該当言語の翻訳リソースが存在すれば、統合する(同じmsgidがあれば上書き)
  718. for (msgid in localizedText) {
  719. multilingualLocalizedTexts[langtag][msgid] = localizedText[msgid];
  720. }
  721. } else {
  722. multilingualLocalizedTexts[langtag] = localizedText;
  723. }
  724. if (language !== langtag) {
  725. // 言語タグに地域下位タグが含まれていれば
  726. // 地域下位タグを取り除いた言語タグも翻訳リソースとして追加する
  727. if (language in multilingualLocalizedTexts) {
  728. // すでに該当言語の翻訳リソースが存在すれば、統合する(同じmsgidがあれば無視)
  729. for (msgid in localizedText) {
  730. if (!(msgid in multilingualLocalizedTexts[language])) {
  731. multilingualLocalizedTexts[language][msgid] = localizedText[msgid];
  732. }
  733. }
  734. } else {
  735. multilingualLocalizedTexts[language] = localizedText;
  736. }
  737. }
  738. // msgidの言語の翻訳リソースを生成
  739. for (msgid in localizedText) {
  740. multilingualLocalizedTexts[ORIGINAL_LOCALE][msgid] = msgid;
  741. }
  742. }
  743. };
  744. })();
  745.  
  746. // Polyfill for Blink
  747. if (!String.prototype.hasOwnProperty('startsWith')) {
  748. /**
  749. * Determines whether a string begins with the characters of another string, returning true or false as appropriate.
  750. * @param {string} searchString - The characters to be searched for at the start of this string.
  751. * @param {number} [position=0] - The position in this string at which to begin searching for searchString.
  752. * @returns {boolean}
  753. * @see {@link http://people.mozilla.org/~jorendorff/es6-draft.html#sec-string.prototype.startswith 21.1.3.18 String.prototype.startsWith (searchString [, position ] )}
  754. * @see {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith String.startsWith - JavaScript | MDN}
  755. * @version polyfill-2013-11-05
  756. * @name String.prototype.startsWith
  757. */
  758. Object.defineProperty(String.prototype, 'startsWith', {
  759. writable: true,
  760. enumerable: false,
  761. configurable: true,
  762. value: function (searchString) {
  763. var position = arguments[1];
  764. return this.indexOf(searchString, position) === Math.max(Math.floor(position) || 0, 0);
  765. },
  766. });
  767. }
  768.  
  769. if (!String.prototype.hasOwnProperty('contains')) {
  770. /**
  771. * Determines whether one string may be found within another string, returning true or false as appropriate.
  772. * @param {string} searchString - A string to be searched for within this string.
  773. * @param {number} [position=0] - The position in this string at which to begin searching for searchString.
  774. * @returns {boolean}
  775. * @see {@link http://people.mozilla.org/~jorendorff/es6-draft.html#sec-string.prototype.contains 21.1.3.6 String.prototype.contains (searchString, position = 0 )}
  776. * @see {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/contains String.contains - JavaScript | MDN}
  777. * @version polyfill-2013-11-05
  778. * @name String.prototype.contains
  779. */
  780. Object.defineProperty(String.prototype, 'contains', {
  781. writable: true,
  782. enumerable: false,
  783. configurable: true,
  784. value: function (searchString) {
  785. return this.indexOf(searchString, arguments[1]) !== -1;
  786. },
  787. });
  788. }
  789.  
  790. }
  791.  
  792. })();