pixiv 検索オプションを追加

This is a search option add-on script for pixiv. Enables you to select search mode by radio buttons (legacy feature).

  1. // ==UserScript==
  2. // @name pixiv 検索オプションを追加
  3. // @name:ja pixiv 検索オプションを追加
  4. // @name:en pixiv Search Options
  5. // @description This is a search option add-on script for pixiv. Enables you to select search mode by radio buttons (legacy feature).
  6. // @description:ja 検索オプションを、以前のようにラジオボタンで選択出来るようにする。
  7. // @namespace http://loda.jp/script/
  8. // @version 6.0.1
  9. // @match https://www.pixiv.net/*
  10. // @exclude https://www.pixiv.net/member_illust.php?*mode=manga*
  11. // @exclude https://www.pixiv.net/apps.php*
  12. // @require https://gitcdn.xyz/cdn/greasemonkey/gm4-polyfill/a834d46afcc7d6f6297829876423f58bb14a0d97/gm4-polyfill.js
  13. // @resource dialog-polyfill.css https://bowercdn.net/c/dialog-polyfill-0.4.10/dialog-polyfill.css
  14. // @require https://bowercdn.net/c/dialog-polyfill-0.4.10/dialog-polyfill.js
  15. // @require https://greasyfork.org/scripts/17895/code/polyfill.js?version=625392
  16. // @require https://greasyfork.org/scripts/19616/code/utilities.js?version=752462
  17. // @require https://greasyfork.org/scripts/17896/code/start-script.js?version=112958
  18. // @license MPL-2.0
  19. // @contributionURL https://www.amazon.co.jp/registry/wishlist/E7PJ5C3K7AM2
  20. // @compatible Edge 非推奨 / Deprecated
  21. // @compatible Firefox
  22. // @compatible Opera
  23. // @compatible Chrome
  24. // @grant GM.getValue
  25. // @grant GM_getValue
  26. // @grant GM.setValue
  27. // @grant GM_setValue
  28. // @grant GM.registerMenuCommand
  29. // @grant GM_registerMenuCommand
  30. // @grant GM.getResourceUrl
  31. // @grant GM_getResourceURL
  32. // @noframes
  33. // @run-at document-start
  34. // @icon data:image/png;base64,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
  35. // @author 100の人
  36. // @homepageURL https://greasyfork.org/scripts/265
  37. // ==/UserScript==
  38.  
  39. // 当スクリプトはpixivが作成、配布しているアプリケーションではありません。
  40. // <https://www.pixiv.net/terms/?page=brand>
  41.  
  42. 'use strict';
  43.  
  44. // L10N
  45. Gettext.setLocalizedTexts({
  46. /*eslint-disable quote-props, max-len */
  47. 'en': {
  48. '完全一致': 'Exact match',
  49. '部分一致': 'Partial match',
  50. 'タイトル・キャプション': 'Title/Description',
  51. '小説': 'Novels',
  52. 'タグ': 'Tags',
  53. 'キーワード': 'Keyword',
  54. '本文': 'Content',
  55. 'ユーザー': 'User',
  56. 'グループ': 'Groups',
  57. 'すべて': 'All',
  58. '検索': 'Search',
  59. '小説検索': 'Search novel',
  60. 'ユーザー検索': 'Search user',
  61. 'グループ検索': 'Search group',
  62. 'pixiv 検索オプションを追加': 'pixiv Search Options',
  63. 'この設定をオンにすると、ヘッダの高さが大きくなります。': 'If you set this, the page header becomes higher.',
  64. '別のタブでpixivを開いていた場合、この設定の変更はそのタブの再読み込み後に反映されます。': 'If you have opened pixiv pages in other tabs, this setting will reflect after the tab refresh.',
  65. 'イラスト・マンガの検索結果ページで、検索モードを維持': 'In search result pages of illusts and mangas, maintains current search mode',
  66. 'この設定をオフにすると、最初は「部分一致」にチェックが入った状態になります。': 'If you unset this, selects “Partial match” radio button by default.',
  67. },
  68. 'fr': {
  69. '完全一致': 'Concordance parfaite',
  70. '部分一致': 'Concordance partielle',
  71. 'タイトル・キャプション': 'Titre, Légende',
  72. '小説': 'Roman',
  73. 'タグ': 'Mots-clés',
  74. 'キーワード': 'Mots-clés',
  75. '本文': 'Contenu',
  76. 'ユーザー': 'Utilisateur',
  77. 'グループ': 'Groups',
  78. 'すべて': 'Tout',
  79. '検索': 'Rechercher',
  80. '小説検索': 'Rechercher un roman',
  81. 'ユーザー検索': '',
  82. 'グループ検索': 'Rechercher un groupe',
  83. 'pixiv 検索オプションを追加': '',
  84. 'この設定をオンにすると、ヘッダの高さが大きくなります。': '',
  85. '別のタブでpixivを開いていた場合、この設定の変更はそのタブの再読み込み後に反映されます。': '',
  86. 'イラスト・マンガの検索結果ページで、検索モードを維持': '',
  87. 'この設定をオフにすると、最初は「部分一致」にチェックが入った状態になります。': '',
  88. },
  89. 'ko': {
  90. '完全一致': '완전 일치',
  91. '部分一致': '부분 일치',
  92. 'タイトル・キャプション': '제목・캡션',
  93. '小説': '소설',
  94. 'タグ': '태그',
  95. 'キーワード': '키워드',
  96. '本文': '본문',
  97. 'ユーザー': '유저',
  98. 'グループ': '그룹',
  99. 'すべて': '전체',
  100. '検索': '검색',
  101. '小説検索': '소설 검색',
  102. 'ユーザー検索': '유저 검색',
  103. 'グループ検索': '그룹 검색',
  104. 'pixiv 検索オプションを追加': '',
  105. 'この設定をオンにすると、ヘッダの高さが大きくなります。': '',
  106. '別のタブでpixivを開いていた場合、この設定の変更はそのタブの再読み込み後に反映されます。': '',
  107. 'イラスト・マンガの検索結果ページで、検索モードを維持': '',
  108. 'この設定をオフにすると、最初は「部分一致」にチェックが入った状態になります。': '',
  109. },
  110. 'ru': {
  111. '完全一致': 'Полное совпадение',
  112. '部分一致': 'Частичное совпадение',
  113. 'タイトル・キャプション': 'Заголовок',
  114. '小説': 'Рассказы',
  115. 'タグ': 'Метка',
  116. 'キーワード': 'Ключевые слова',
  117. '本文': 'Текст',
  118. 'ユーザー': 'Пользователь',
  119. 'グループ': 'Группа',
  120. 'すべて': 'Все',
  121. '検索': 'Поиск',
  122. '小説検索': 'Искать рассказ',
  123. 'ユーザー検索': 'Искать пользователя',
  124. 'グループ検索': 'Искать группу',
  125. 'pixiv 検索オプションを追加': '',
  126. 'この設定をオンにすると、ヘッダの高さが大きくなります。': '',
  127. '別のタブでpixivを開いていた場合、この設定の変更はそのタブの再読み込み後に反映されます。': '',
  128. 'イラスト・マンガの検索結果ページで、検索モードを維持': '',
  129. 'この設定をオフにすると、最初は「部分一致」にチェックが入った状態になります。': '',
  130. },
  131. 'th': {
  132. '完全一致': '',
  133. '部分一致': '',
  134. 'タイトル・キャプション': 'ชื่อและคำบรรยาย',
  135. '小説': 'นิยาย',
  136. 'タグ': 'แท็ก',
  137. 'キーワード': 'คีย์เวิร์ด',
  138. '本文': '',
  139. 'ユーザー': 'ผู้ใช้',
  140. 'グループ': '',
  141. 'すべて': 'ทั้งหมด',
  142. '検索': 'ค้นหา',
  143. '小説検索': '',
  144. 'ユーザー検索': '',
  145. 'グループ検索': '',
  146. 'pixiv 検索オプションを追加': '',
  147. 'この設定をオンにすると、ヘッダの高さが大きくなります。': '',
  148. '別のタブでpixivを開いていた場合、この設定の変更はそのタブの再読み込み後に反映されます。': '',
  149. 'イラスト・マンガの検索結果ページで、検索モードを維持': '',
  150. 'この設定をオフにすると、最初は「部分一致」にチェックが入った状態になります。': '',
  151. },
  152. 'zh': {
  153. '完全一致': '完全相同',
  154. '部分一致': '部分相同',
  155. 'タイトル・キャプション': '题目/简述',
  156. '小説': '小说',
  157. 'タグ': '标签',
  158. 'キーワード': '关键词',
  159. '本文': '内容',
  160. 'ユーザー': '用户',
  161. 'グループ': '群组',
  162. 'すべて': '全部',
  163. '検索': '搜索',
  164. '小説検索': '搜索小说',
  165. 'ユーザー検索': '搜索用户',
  166. 'グループ検索': '搜索群组',
  167. 'pixiv 検索オプションを追加': '',
  168. 'この設定をオンにすると、ヘッダの高さが大きくなります。': '',
  169. '別のタブでpixivを開いていた場合、この設定の変更はそのタブの再読み込み後に反映されます。': '',
  170. 'イラスト・マンガの検索結果ページで、検索モードを維持': '',
  171. 'この設定をオフにすると、最初は「部分一致」にチェックが入った状態になります。': '',
  172. },
  173. 'zh-tw': {
  174. '完全一致': '完全相同',
  175. '部分一致': '部分相同',
  176. 'タイトル・キャプション': '題目/簡述',
  177. '小説': '小說',
  178. 'タグ': '標籤',
  179. 'キーワード': '關鍵詞',
  180. '本文': '內容',
  181. 'ユーザー': '用戶',
  182. 'グループ': '群組',
  183. 'すべて': '全部',
  184. '検索': '搜索',
  185. '小説検索': '搜索小說',
  186. 'ユーザー検索': '搜索用戶',
  187. 'グループ検索': '搜索群組',
  188. 'pixiv 検索オプションを追加': '',
  189. 'この設定をオンにすると、ヘッダの高さが大きくなります。': '',
  190. '別のタブでpixivを開いていた場合、この設定の変更はそのタブの再読み込み後に反映されます。': '',
  191. 'イラスト・マンガの検索結果ページで、検索モードを維持': '',
  192. 'この設定をオフにすると、最初は「部分一致」にチェックが入った状態になります。': '',
  193. },
  194. 'es': {
  195. '完全一致': 'Coincidencia exacta',
  196. '部分一致': 'Coincidencia parcial',
  197. 'タイトル・キャプション': 'Título/Descripción',
  198. '小説': 'Novelas',
  199. 'タグ': 'Etiquetas',
  200. 'キーワード': 'Palabra clave',
  201. '本文': 'Mensaje',
  202. 'ユーザー': 'Usuarios',
  203. 'グループ': 'Grupo',
  204. 'すべて': 'Todos',
  205. '検索': 'Buscar',
  206. '小説検索': 'Buscar novela',
  207. 'ユーザー検索': 'Buscar usuario',
  208. 'グループ検索': 'Buscar grupo',
  209. 'pixiv 検索オプションを追加': '',
  210. 'この設定をオンにすると、ヘッダの高さが大きくなります。': '',
  211. '別のタブでpixivを開いていた場合、この設定の変更はそのタブの再読み込み後に反映されます。': '',
  212. 'イラスト・マンガの検索結果ページで、検索モードを維持': '',
  213. 'この設定をオフにすると、最初は「部分一致」にチェックが入った状態になります。': '',
  214. },
  215. /*eslint-enable quote-props, max-len */
  216. });
  217.  
  218. /**
  219. * スクリプトの起動。
  220. */
  221. class PixivSearchOptions
  222. {
  223. constructor()
  224. {
  225. this.id = Math.random();
  226.  
  227. this.observeURLChanged();
  228.  
  229. startScript(
  230. () => {
  231. if (document.body.classList.contains('not-logged-in')) {
  232. return;
  233. }
  234.  
  235. /**
  236. * 検索窓。
  237. * @type {HTMLInputElement}
  238. * @access protected
  239. */
  240. this.word = document.querySelector(
  241. '#root form input[type="text"], #js-mount-point-header form input[type="text"]' // 後者は仕様変更前
  242. );
  243.  
  244. this.main();
  245. },
  246. parent => parent.matches('#root, #js-mount-point-header div'), // 後者は仕様変更前
  247. target => target.matches('#root > div:first-child')
  248. || /* 仕様変更前 */ target.firstElementChild && target.firstElementChild.localName === 'form',
  249. () => document.querySelector(
  250. '#root form input[type="text"], #js-mount-point-header form input[type="text"]' // 後者は仕様変更前
  251. ),
  252. {},
  253. 60 * 1000
  254. );
  255. }
  256.  
  257. observeURLChanged()
  258. {
  259. addEventListener('message', this);
  260.  
  261. GreasemonkeyUtils.executeOnUnsafeContext(function (id) {
  262. History.prototype.pushState = new Proxy(History.prototype.pushState, {
  263. apply(target, thisArg, argumentsList)
  264. {
  265. postMessage({ id, url: argumentsList[2] }, location.origin);
  266. return Reflect.apply(target, thisArg, argumentsList);
  267. },
  268. });
  269. }, [this.id]);
  270. }
  271.  
  272. /**
  273. * @param {Event}
  274. */
  275. handleEvent(event)
  276. {
  277. switch (event.type) {
  278. case 'message': {
  279. if (event.origin !== location.origin || typeof event.data !== 'object' || event.data === null
  280. || event.data.id !== this.id || !event.data.url) {
  281. break;
  282. }
  283. this.reflectSearchMode(new URL(event.data.url, location));
  284. break;
  285. }
  286. case 'submit': {
  287. const form = event.target;
  288. if (this.word.value === '') {
  289. break;
  290. }
  291.  
  292. const searchMode = form.querySelector('[name="s_mode"]:checked');
  293. const subOption = form.querySelector('[name="s_sub_mode"]:checked');
  294.  
  295. if (searchMode.value === 's_tag' || searchMode.value === 's_novel' && subOption.value === 's_tag') {
  296. // 検索窓のデフォルトの検索モードなら
  297. break;
  298. }
  299.  
  300. event.stopPropagation();
  301.  
  302. if (searchMode.dataset.formAction) {
  303. form.action = searchMode.dataset.formAction;
  304. }
  305. this.word.name = searchMode.dataset.wordName || 'word';
  306. if (searchMode.dataset.modeName) {
  307. searchMode.name = searchMode.dataset.modeName;
  308. }
  309.  
  310. const novel = searchMode.value === 's_novel';
  311. if (subOption) {
  312. // 副検索モードが存在すれば
  313. searchMode.value = subOption.value;
  314. subOption.removeAttribute('name');
  315. }
  316.  
  317. if (['s_usr', 's_group'].includes(searchMode.value)) {
  318. // ユーザー、グループの検索なら
  319. if (form.action !== location.origin + location.pathname) {
  320. // サービス間をまたぐ場合、不要なパラメーターは送信しない
  321. for (const hiddenParam of form.querySelectorAll('[type="hidden"]')) {
  322. hiddenParam.disabled = true;
  323. }
  324. }
  325. break;
  326. }
  327.  
  328. const result = /^\/tags\/[^/]+\/([a-z]+)$/.exec(location.pathname);
  329. const currentMode = result ? result[1] : '';
  330. if (!currentMode || currentMode !== 'novels' && novel || currentMode === 'novels' && !novel
  331. || decodeURIComponent(/^\/tags\/([^/]+)/u.exec(location.pathname)[1]) !== this.word.value) {
  332. // 検索結果以外のページ、サービス間をまたぐ場合、
  333. // またはキーワードが現在のページと異なる場合
  334. form.action = `/tags/${encodeURIComponent(this.word.value)}/${novel ? 'novels' : 'artworks'}`;
  335. this.word.removeAttribute('name');
  336. if (searchMode.value === 's_tag_full') {
  337. // タグ完全一致なら、s_modeを削除
  338. searchMode.removeAttribute('name');
  339. }
  340. break;
  341. }
  342.  
  343. // イラストからイラスト、小説から小説の場合
  344. event.preventDefault();
  345.  
  346. // 検索オプションボタンをクリック
  347. document.querySelector('[d^="M0 1C0 0.447754"]').closest('button').click();
  348. new MutationObserver((mutations, observer) => {
  349. let dialog;
  350. for (const mutation of mutations) {
  351. dialog = Array.from(mutation.addedNodes).find(
  352. node => node.nodeType === Node.ELEMENT_NODE && node.getAttribute('role') === 'presentation'
  353. );
  354. if (dialog) {
  355. break;
  356. }
  357. }
  358. if (!dialog) {
  359. return;
  360. }
  361. observer.disconnect();
  362.  
  363. dialog.hidden = true;
  364.  
  365. // 対象
  366. const input = dialog.querySelectorAll('[class$="-dummyInput"]')[novel ? 0 : 1];
  367. // プルダウンメニューを開く
  368. input.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: ' ' }));
  369. new MutationObserver(function (mutations, observer) {
  370. let select, options;
  371. mutations: for (const mutation of mutations) {
  372. for (const node of mutation.addedNodes) {
  373. options = node.querySelectorAll('[role="option"]');
  374. if (options.length === 0) {
  375. continue;
  376. }
  377. select = node;
  378. break mutations;
  379. }
  380. }
  381. if (!select) {
  382. return;
  383. }
  384. observer.disconnect();
  385.  
  386. // 項目を選択する
  387. options[(novel
  388. ? ['s_tag_only', 's_tag_full', 's_tc', 's_tag']
  389. : ['s_tag', 's_tag_full', 's_tc']).indexOf(searchMode.value)].click();
  390. new MutationObserver(function (mutations, observer) {
  391. if (!mutations.some(mutation => Array.from(mutation.removedNodes).includes(select))) {
  392. return;
  393. }
  394. observer.disconnect();
  395. setTimeout(function () {
  396. // 「適用する」ボタンを押す
  397. dialog.querySelector('[type="submit"]').click();
  398. }, 100);
  399. }).observe(select.parentElement, { childList: true });
  400. }).observe(dialog, { subtree: true, childList: true });
  401. }).observe(document.body, { childList: true });
  402. break;
  403. }
  404. case 'change': {
  405. // 副検索モードの選択
  406. const subOptions = event.target.parentElement.parentElement;
  407. if (event.target.name === 's_mode') {
  408. // 検索モードの選択なら
  409. if (subOptions.classList.contains('sub-options')) {
  410. // 副検索モードが存在すれば
  411. subOptions.querySelector('[value="s_tag"], [value="keyword_all"]').checked = true;
  412. } else {
  413. const subOption = event.currentTarget.querySelector('[name="s_sub_mode"]:checked');
  414. if (subOption) {
  415. subOption.checked = false;
  416. }
  417. }
  418. this.word.placeholder = event.target.dataset.placeholder;
  419. } else if (event.target.name === 's_sub_mode') {
  420. // 副検索モードの選択なら
  421. const subOption = subOptions.firstElementChild.firstElementChild;
  422. subOption.checked = true;
  423. this.word.placeholder = subOption.dataset.placeholder;
  424. }
  425. break;
  426. }
  427. case 'dblclick':
  428. // ラベルをダブルクリックで検索
  429. if (event.target.matches('label, label *')) {
  430. event.currentTarget.dispatchEvent(new Event('submit'));
  431. }
  432. break;
  433. }
  434. }
  435.  
  436. main()
  437. {
  438. // 言語の設定
  439. Gettext.setLocale(document.documentElement.lang);
  440.  
  441. /**
  442. * 検索オプションを設定するラジオボタンのHTML文字列。
  443. * @type {string}
  444. */
  445. const optionsHTML = h`
  446. <label>
  447. <input data-placeholder="${_('検索')}" value="s_tag_full" name="s_mode" type="radio" />
  448. ${_('完全一致')}
  449. </label>
  450. <label>
  451. <input data-placeholder="${_('検索')}" value="s_tag" name="s_mode" type="radio" />
  452. ${_('部分一致')}
  453. </label>
  454. <label>
  455. <input data-placeholder="${_('検索')}" value="s_tc" name="s_mode" type="radio" />
  456. ${_('タイトル・キャプション')}
  457. </label>
  458. <div id="s_novel" class="sub-options" title="${_('小説')}">
  459. <label>
  460. <input data-placeholder="${_('小説検索')}" value="s_novel" name="s_mode" type="radio" />
  461. <img alt="${_('小説')}" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAARBAMAAADNtor0AAAAD1BMVEVldC2Spb6susrCzdr9//waY6iPAAAAAXRSTlMAQObYZgAAAEFJREFUCNdjYBAUZIABERdDBNNZAM6ECQsCmY6CQCDAIAJiugABJtPZ2NiEfFFDIROYqIuzMkzUxcXZCMIUhAEBACaMGgfSfm2+AAAAAElFTkSuQmCC" />
  462. </label>
  463. <label>
  464. <input value="s_tag_full" name="s_sub_mode" type="radio" />
  465. ${_('タグ')}
  466. </label>
  467. <label>
  468. <input value="s_tag" name="s_sub_mode" type="radio" />
  469. ${_('キーワード')}
  470. </label>
  471. <label>
  472. <input value="s_tc" name="s_sub_mode" type="radio" />
  473. ${_('本文')}
  474. </label>
  475. </div>
  476. <label title="${_('ユーザー')}">
  477. <input data-placeholder="${_('ユーザー検索')}" data-word-name="nick"
  478. data-form-action="https://www.pixiv.net/search_user.php" value="s_usr" name="s_mode" type="radio" />
  479. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="16px" height="16px">
  480. <title>${_('ユーザー')}</title>
  481. <path fill="#809DB8" d="M848,348 l98,-20 q28,0 54,22 q38,32 48,44 q22,26 34,26 q16,0 26,-4 q14,-6 52,2 q32,6 48,24 q18,20 23,42 t5,50 q0,32 2,48 q-4,24 -2,38 q6,36 6,62 t-6,44 t12,28 t38,10 h22 q28,0 40,46 q8,30 8,60 q0,14 -2,30 l-6,46 q-2,16 -6,22 q-6,10 -10,16 t-14,14 l-18,12 l-24,16 q-18,10 -30,18 q-22,62 -50,102 q-34,48 -58,70 l-22,22 q-10,96 -10,118 v6 q0,8 2,16 q0,8 4,22 q4,18 12,28 l20,28 q10,14 30,28 q8,6 46,26 q26,14 62,24 q46,12 82,18 q88,12 138,28 q60,18 102,44 t62,40 t50,46 q28,30 30,32 q20,22 40,98 q20,74 20,140 v66 h-1768 v-72 q0,-42 14,-126 q16,-92 34,-106 q8,-6 32,-34 t46,-48 t62,-46 q36,-24 100,-44 q66,-22 140,-32 q60,-8 102,-22 q40,-12 74,-32 t50,-34 q26,-24 34,-36 q10,-16 16,-30 q12,-18 10,-20 q-4,-6 -2,-6 v-132 q-4,-2 -12,-8 q-8,-4 -28,-22 q-14,-10 -40,-36 q-18,-18 -38,-50 t-32,-64 l-36,-18 q-20,-10 -24,-14 q-4,-2 -12,-18 q-8,-14 -8,-16 q0,-4 -2,-18 t-4,-22 q0,-4 2,-10 t5,-26 t3,-38 q0,-20 8,-38 q10,-32 12,-36 q8,-14 18,-24 t22,-10 h14 q8,-2 18,-12 q12,-12 8,-30 q2,-64 2,-134 q0,-22 8,-50 q10,-38 18,-58 q12,-32 38,-56 t60,-32 l52,-12 h-2 q2,0 6,-2 q10,-2 12,-2 v-6 v-4 q-2,-4 0,-6 q0,-2 8,-4 t22,-2 z" />
  482. </svg>
  483. </label>
  484. <div id="s_group" class="sub-options" title="${_('グループ')}">
  485. <label>
  486. <input data-placeholder="${_('グループ検索')}" data-mode-name="mode"
  487. data-form-action="https://www.pixiv.net/group/search_group.php"
  488. value="s_group" name="s_mode" type="radio" />
  489. <img alt="${_('グループ')}" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEWLpryswdDV3ubr8POkfP3KAAAAT0lEQVQI12NYBQQMYGLd7/e/GFYtX/6LYfX27fsYVqU/i2JYwX4hjGEJk4MYw0Y2hSiGLUwNcgzrVr9aBxLjY1jCDOJGP93FsPrf/ldgUwDsuCQfQnDv1AAAAABJRU5ErkJggg==" />
  490. </label>
  491. <label>
  492. <input value="keyword_all" name="s_sub_mode" type="radio" />
  493. ${_('すべて')}
  494. </label>
  495. <label>
  496. <input value="tag_full" name="s_sub_mode" type="radio" />
  497. ${_('完全一致')}
  498. </label>
  499. <label>
  500. <input value="tag" name="s_sub_mode" type="radio" />
  501. ${_('部分一致')}
  502. </label>
  503. <label>
  504. <input value="keyword" name="s_sub_mode" type="radio" />
  505. ${_('タイトル・キャプション')}
  506. </label>
  507. </div>`;
  508.  
  509. const form = this.word.form;
  510. form.id = 'suggest-container';
  511. this.style({
  512. gridElementSelector: Array.from(form.parentElement.parentElement.classList).map(cls => '.' + cls).join(''),
  513. });
  514.  
  515. // 検索オプションを設定するラジオボタンの追加
  516. form.insertAdjacentHTML('beforeend', optionsHTML);
  517.  
  518. form.parentElement.addEventListener('submit', this, true);
  519. form.addEventListener('change', this);
  520. form.addEventListener('dblclick', this);
  521.  
  522. // 現在の検索モードを設定
  523. this.reflectSearchMode(location);
  524. }
  525.  
  526. /**
  527. * 現在のページの検索モードをもとに、検索窓の検索モードを設定します。
  528. * @param {string} url
  529. * @returns {void}
  530. */
  531. reflectSearchMode(url)
  532. {
  533. const { pathname, searchParams } = new URL(url);
  534.  
  535. let mode, subMode;
  536.  
  537. const form = this.word.form;
  538.  
  539. if (pathname.startsWith('/tags/')) {
  540. mode = searchParams.get('s_mode');
  541. if (pathname.endsWith('novels')) {
  542. // 小説の検索結果
  543. subMode = mode;
  544. mode = 's_novel';
  545. } else {
  546. // イラストの検索結果
  547. const currentMode = mode || 's_tag_full';
  548. mode = 's_tag';
  549.  
  550. GM.getValue('check-match-full-by-default-on-full-match-result', false).then(function (value) {
  551. if (value) {
  552. form.querySelector(`[value="${currentMode}"]`).click();
  553. }
  554. });
  555. }
  556. } else if (pathname.startsWith('/novel/')) {
  557. mode = 's_novel';
  558. } else if (pathname === '/search_user.php') {
  559. mode = 's_usr';
  560. } else if (pathname.startsWith('/group/')) {
  561. mode = 's_group';
  562. } else {
  563. mode = 's_tag';
  564. }
  565. form.querySelector(!subMode ? `[value="${mode}"]` : `#${mode} [value="${subMode}"]`).click();
  566. }
  567.  
  568. /**
  569. * スタイルシートを設定します。
  570. * @access protected
  571. */
  572. style({ gridElementSelector })
  573. {
  574. document.head.insertAdjacentHTML('beforeend', `<style>
  575. ${gridElementSelector} {
  576. grid-template-columns: 1fr minmax(auto, 970px) 1fr;
  577. }
  578.  
  579. #suggest-container {
  580. display: flex;
  581. align-items: center;
  582. justify-content: center;
  583. }
  584.  
  585. /*------------------------------------
  586. 検索オプション
  587. */
  588. #suggest-container > div:first-of-type {
  589. padding-right: 1em;
  590. }
  591. #suggest-container label {
  592. padding: 0 0.7em;
  593. display: flex;
  594. align-items: center;
  595. white-space: nowrap;
  596. }
  597. #suggest-container label input {
  598. margin-right: 0.3em;
  599. width: initial;
  600. }
  601.  
  602. /*------------------------------------
  603. 副検索モード
  604. */
  605. #suggest-container .sub-options label:not(:first-of-type) {
  606. display: none;
  607. position: absolute;
  608. z-index: 1;
  609. width: 13em;
  610. height: 2em;
  611. border: solid 1px #D6DEE5;
  612. border-top-style: none;
  613. border-bottom-style: none;
  614. background: #FFFFFF;
  615. }
  616. #suggest-container .sub-options label:nth-of-type(2) {
  617. border-top-style: solid;
  618. border-radius: 5px 5px 0 0;
  619. }
  620. #suggest-container .sub-options label:nth-of-type(3) {
  621. margin-top: 2em;
  622. }
  623. #suggest-container .sub-options label:nth-of-type(4) {
  624. margin-top: 4em;
  625. }
  626. #suggest-container .sub-options label:nth-of-type(5) {
  627. margin-top: 6em;
  628. }
  629. #suggest-container .sub-options label:last-of-type {
  630. border-bottom-style: solid;
  631. border-radius: 0 0 5px 5px;
  632. }
  633. #suggest-container .sub-options:hover label {
  634. display: flex;
  635. }
  636. #suggest-container .sub-options:hover::after {
  637. display: block;
  638. }
  639. </style>`);
  640. }
  641. }
  642.  
  643. const pixivSearchOptions = new PixivSearchOptions();
  644.  
  645. /**
  646. * 設定を管理します。
  647. */
  648. class PixivSearchOptionsSettings
  649. {
  650. constructor()
  651. {
  652. startScript(
  653. async () => {
  654. GM.registerMenuCommand(_('pixiv 検索オプションを追加'), () => this.showDialog());
  655. },
  656. parent => parent === document.documentElement,
  657. target => target === document.body,
  658. () => document.body
  659. );
  660. }
  661.  
  662. /**
  663. * 設定ダイアログを表示します。
  664. */
  665. async showDialog()
  666. {
  667. if (!this.dialog) {
  668. this.initialize();
  669. }
  670.  
  671. const form = document.forms['pixiv-search-options-settings'];
  672. form['check-match-full-by-default-on-full-match-result'].checked
  673. = await GM.getValue('check-match-full-by-default-on-full-match-result', false);
  674.  
  675. this.dialog.showModal();
  676. }
  677.  
  678. /**
  679. * @param {Event} event
  680. */
  681. handleEvent(event)
  682. {
  683. switch (event.type) {
  684. case 'change':
  685. GM.setValue(event.target.name, event.target.checked);
  686. break;
  687.  
  688. case 'click':
  689. this.dialog.close();
  690. break;
  691. }
  692. }
  693.  
  694. /**
  695. * 設定ダイアログを構築します。
  696. * @access private
  697. */
  698. initialize()
  699. {
  700. document.head.insertAdjacentHTML('beforeend', `<style>
  701. /*====================================
  702. pixiv 検索オプションを表示 設定ダイアログ
  703. */
  704. #pixiv-search-options-settings {
  705. border-radius: 0.5em;
  706. border-color: #69AFCA;
  707. border-width: 0.5em;
  708. }
  709.  
  710. #pixiv-search-options-settings h1 {
  711. font-size: 1.5em;
  712. font-weight: bold;
  713. margin-bottom: 0.5em;
  714. text-align: center;
  715. }
  716.  
  717. #pixiv-search-options-settings form {
  718. color: #333333;
  719. }
  720.  
  721. #pixiv-search-options-settings form ul li {
  722. margin: 0.2em 0;
  723. }
  724.  
  725. #pixiv-search-options-settings form label {
  726. display: flex;
  727. align-items: center;
  728. }
  729.  
  730. #pixiv-search-options-settings form small {
  731. margin-left: 1.3em;
  732. }
  733.  
  734. #pixiv-search-options-settings form [name="close"] {
  735. border: none;
  736. position: absolute;
  737. border-radius: 50%;
  738. position: absolute;
  739. top: -1em;
  740. right: -1em;
  741. width: 2.5em;
  742. height: 2.5em;
  743. background:
  744. black url("https://source.pixiv.net/www/images/common/icon_modal_close.png") no-repeat 50% 50%;
  745. cursor: pointer;
  746. overflow: hidden;
  747. white-space: nowrap;
  748. text-indent: 200%;
  749. }
  750. </style>`);
  751.  
  752. document.body.insertAdjacentHTML('afterbegin', h`<dialog id="pixiv-search-options-settings">
  753. <h1>${_('pixiv 検索オプションを追加')}</h1>
  754. <form name="pixiv-search-options-settings">
  755. <ul>
  756. <li>
  757. <label>
  758. <input type="checkbox" name="check-match-full-by-default-on-full-match-result" />
  759. ${_('イラスト・マンガの検索結果ページで、検索モードを維持')}
  760. </label>
  761. <p><small class="settingColor">${_('この設定をオフにすると、最初は「部分一致」にチェックが入った状態になります。')}</small></p>
  762. </li>
  763. </ul>
  764. <button type="button" name="close">${_('閉じる')}</button>
  765. </form>
  766. </dialog>`);
  767.  
  768. this.dialog = document.getElementById('pixiv-search-options-settings');
  769. this.polyfillDialogElements([this.dialog]);
  770.  
  771. const form = document.forms['pixiv-search-options-settings'];
  772. form.addEventListener('change', this);
  773. form.close.addEventListener('click', this);
  774. }
  775.  
  776. /**
  777. * Microsoft Edge、およびFirefox向けに、dialog要素のpolyfillを行います。
  778. * @access private
  779. * @param {HTMLDialogElement[]} dialogs
  780. * @returns {Promise.<void>}
  781. */
  782. async polyfillDialogElements(dialogs)
  783. {
  784. document.head.insertAdjacentHTML(
  785. 'beforeend',
  786. h`<link rel="stylesheet" href="${await GM.getResourceUrl('dialog-polyfill.css')}" />`
  787. );
  788.  
  789. for (const dialog of dialogs) {
  790. /*globals dialogPolyfill */
  791. dialogPolyfill.registerDialog(dialog);
  792. }
  793. }
  794. }
  795.  
  796. new PixivSearchOptionsSettings();