Greasy Fork is available in English.

Twitter Middle Clicks

This script makes it possible to open a quoted post, a trend, a link in post input form, an input complement, or an image on the image dialog into a new tab by middle click.

  1. // ==UserScript==
  2. // @name Twitter Middle Clicks
  3. // @name:en X/Twitter Middle Clicks
  4. // @name:ja X/Twitter 中クリック
  5. // @description This script makes it possible to open a quoted post, a trend, a link in post input form, an input complement, or an image on the image dialog into a new tab by middle click.
  6. // @description:ja 引用されたポスト、トレンド、ポスト本文入力欄のリンク、入力補完、画像ダイアログの画像を中クリックして新規タブで開けるようにします。
  7. // @namespace https://greasyfork.org/users/137
  8. // @version 1.5.0
  9. // @match https://twitter.com/*
  10. // @match https://x.com/*
  11. // @exclude https://twitter.com/*/tos*
  12. // @exclude https://twitter.com/*/privacy*
  13. // @exclude https://twitter.com/i/cards/*
  14. // @license MPL-2.0
  15. // @contributionURL https://github.com/sponsors/esperecyan
  16. // @compatible Edge
  17. // @compatible Firefox Firefoxを推奨 / Firefox is recommended
  18. // @compatible Opera
  19. // @compatible Chrome
  20. // @grant dummy
  21. // @run-at document-start
  22. // @icon https://abs.twimg.com/favicons/twitter.ico
  23. // @author 100の人
  24. // @homepageURL https://greasyfork.org/scripts/392927
  25. // ==/UserScript==
  26.  
  27. 'use strict';
  28.  
  29. addEventListener('mouseup', function (event) { // Firefox、Google Chromeは、auxclickがリンク上でしか動作しない不具合がある
  30. if (event.button !== 1 || event.detail !== 0 || event.target.closest('a')) {
  31. // 中クリックでない、ダブルクリック、またはリンクのクリックなら
  32. return;
  33. }
  34.  
  35. if (event.target.localName === 'img' && event.target.matches('[data-testid="swipe-to-dismiss"] *')) {
  36. // 画像ダイアログの画像
  37. open(event.target.src);
  38. return;
  39. }
  40.  
  41. if (event.target.dataset.text) {
  42. if (!event.target.parentElement.parentElement.style.color) {
  43. return;
  44. }
  45.  
  46. // ポスト本文入力欄のリンク
  47. let url;
  48. const content = event.target.textContent;
  49. if (content.startsWith('@')) {
  50. url = '/' + content.replace('@', '');
  51. } else if (content.startsWith('#')) {
  52. // ハッシュタグ
  53. url = '/hashtag/' + encodeURIComponent(content.replace('#', ''));
  54. } else if (content.startsWith('$')) {
  55. // キャッシュタグ
  56. url = '/search?q=' + encodeURIComponent(content);
  57. } else {
  58. try {
  59. new URL(content);
  60. } catch (exception) {
  61. if (exception.name !== 'TypeError') {
  62. throw exception;
  63. }
  64. // ドメイン
  65. url = 'http://' + content;
  66. }
  67.  
  68. if (!url) {
  69. // URL
  70. url = content;
  71. }
  72. }
  73. open(url);
  74. return;
  75. }
  76.  
  77. const option = event.target.closest('[role="option"]');
  78. if (option) {
  79. // ポスト本文入力欄、または検索窓の入力補完
  80. let url;
  81. if (option.querySelector('[data-testid="TypeaheadUser"]')) {
  82. // ユーザー
  83. url = '/' + option.querySelectorAll('[dir="ltr"]')[2].textContent.replace('@', '');
  84. } else {
  85. let content = option.querySelector('[dir="ltr"]').textContent;
  86. const searchForm = option.closest('[role="search"]');
  87. if (searchForm) {
  88. // 検索窓
  89. const searchTerms = searchForm.querySelector('[role="combobox"]').value;
  90. switch (content) {
  91. case `「${searchTerms}」を検索`:
  92. content = searchTerms;
  93. break;
  94. case `@${searchTerms}さんのプロフィール`:
  95. url = '/' + searchTerms.replace('@', '');
  96. break;
  97. }
  98. }
  99. if (!url) {
  100. url = content.startsWith('#')
  101. ? '/hashtag/' + encodeURIComponent(content.replace('#', '')) // ハッシュタグ
  102. : '/search?q=' + encodeURIComponent(content); // Xの検索窓では空白が「+」ではなく「%20」に置き換わる
  103. }
  104. }
  105. open(url);
  106. return;
  107. }
  108.  
  109. // Ctrl + 主クリック
  110. const init = {};
  111. for (const key in event) {
  112. init[key] = event[key];
  113. }
  114. init.button = 0;
  115. init.ctrlKey = true;
  116. if (!event.target.dispatchEvent(new MouseEvent('click', init))) {
  117. event.preventDefault();
  118. event.stopImmediatePropagation();
  119. }
  120. }, true);