🔗 链接助手

支持所有网站在新标签页中打开第三方网站链接,在新标签页中打开符合指定规则的链接

当前为 2023-04-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name 🔗 Links Helper
  3. // @name:zh-CN 🔗 链接助手
  4. // @namespace https://github.com/utags/links-helper
  5. // @homepage https://github.com/utags/links-helper#readme
  6. // @supportURL https://github.com/utags/links-helper/issues
  7. // @version 0.1.0
  8. // @description Open external links in a new tab, open links matching the specified rules in a new tab
  9. // @description:zh-CN 支持所有网站在新标签页中打开第三方网站链接,在新标签页中打开符合指定规则的链接
  10. // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMTUnIGhlaWdodD0nMTUnIHZpZXdCb3g9JzAgMCAxNSAxNScgZmlsbD0nbm9uZScgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cGF0aCBkPSdNMyAyQzIuNDQ3NzIgMiAyIDIuNDQ3NzIgMiAzVjEyQzIgMTIuNTUyMyAyLjQ0NzcyIDEzIDMgMTNIMTJDMTIuNTUyMyAxMyAxMyAxMi41NTIzIDEzIDEyVjguNUMxMyA4LjIyMzg2IDEyLjc3NjEgOCAxMi41IDhDMTIuMjIzOSA4IDEyIDguMjIzODYgMTIgOC41VjEySDNWM0w2LjUgM0M2Ljc3NjE0IDMgNyAyLjc3NjE0IDcgMi41QzcgMi4yMjM4NiA2Ljc3NjE0IDIgNi41IDJIM1pNMTIuODUzNiAyLjE0NjQ1QzEyLjkwMTUgMi4xOTQzOSAxMi45Mzc3IDIuMjQ5NjQgMTIuOTYyMSAyLjMwODYxQzEyLjk4NjEgMi4zNjY2OSAxMi45OTk2IDIuNDMwMyAxMyAyLjQ5N0wxMyAyLjVWMi41MDA0OVY1LjVDMTMgNS43NzYxNCAxMi43NzYxIDYgMTIuNSA2QzEyLjIyMzkgNiAxMiA1Ljc3NjE0IDEyIDUuNVYzLjcwNzExTDYuODUzNTUgOC44NTM1NUM2LjY1ODI5IDkuMDQ4ODIgNi4zNDE3MSA5LjA0ODgyIDYuMTQ2NDUgOC44NTM1NUM1Ljk1MTE4IDguNjU4MjkgNS45NTExOCA4LjM0MTcxIDYuMTQ2NDUgOC4xNDY0NUwxMS4yOTI5IDNIOS41QzkuMjIzODYgMyA5IDIuNzc2MTQgOSAyLjVDOSAyLjIyMzg2IDkuMjIzODYgMiA5LjUgMkgxMi40OTk5SDEyLjVDMTIuNTY3OCAyIDEyLjYzMjQgMi4wMTM0OSAxMi42OTE0IDIuMDM3OTRDMTIuNzUwNCAyLjA2MjM0IDEyLjgwNTYgMi4wOTg1MSAxMi44NTM2IDIuMTQ2NDVaJyBmaWxsPSdjdXJyZW50Q29sb3InIGZpbGwtcnVsZT0nZXZlbm9kZCcgY2xpcC1ydWxlPSdldmVub2RkJz48L3BhdGg+PC9zdmc+
  11. // @author Pipecraft
  12. // @license MIT
  13. // @match https://*/*
  14. // @match http://*/*
  15. // @run-at document-end
  16. // @grant GM_addElement
  17. // @grant GM_addStyle
  18. // @grant GM_registerMenuCommand
  19. // @grant GM_getValue
  20. // @grant GM_setValue
  21. // @grant GM_addValueChangeListener
  22. // @grant GM_removeValueChangeListener
  23. // ==/UserScript==
  24. //
  25. //// Recent Updates
  26. //// - 0.1.0 2023.04.23
  27. //// - Setting for url rules, open links matching the specified rules in a new tab
  28. //// - 0.0.2 2023.04.22
  29. //// - Add settings menu
  30. //// - Enable/Disable userscript
  31. //// - Enable/Disable current site
  32. ////
  33. ;(() => {
  34. "use strict"
  35. var doc = document
  36. var $ = (element, selectors) =>
  37. element && typeof element === "object"
  38. ? element.querySelector(selectors)
  39. : doc.querySelector(element)
  40. var $$ = (element, selectors) =>
  41. element && typeof element === "object"
  42. ? [...element.querySelectorAll(selectors)]
  43. : [...doc.querySelectorAll(element)]
  44. var createElement = (tagName, attributes) =>
  45. setAttributes(doc.createElement(tagName), attributes)
  46. var addEventListener = (element, type, listener, options) => {
  47. if (!element) {
  48. return
  49. }
  50. if (typeof type === "object") {
  51. for (const type1 in type) {
  52. if (Object.hasOwn(type, type1)) {
  53. element.addEventListener(type1, type[type1])
  54. }
  55. }
  56. } else if (typeof type === "string" && typeof listener === "function") {
  57. element.addEventListener(type, listener, options)
  58. }
  59. }
  60. var removeEventListener = (element, type, listener, options) => {
  61. if (!element) {
  62. return
  63. }
  64. if (typeof type === "object") {
  65. for (const type1 in type) {
  66. if (Object.hasOwn(type, type1)) {
  67. element.removeEventListener(type1, type[type1])
  68. }
  69. }
  70. } else if (typeof type === "string" && typeof listener === "function") {
  71. element.removeEventListener(type, listener, options)
  72. }
  73. }
  74. var getAttribute = (element, name) =>
  75. element ? element.getAttribute(name) : null
  76. var setAttribute = (element, name, value) =>
  77. element ? element.setAttribute(name, value) : void 0
  78. var setAttributes = (element, attributes) => {
  79. if (element && attributes) {
  80. for (const name in attributes) {
  81. if (Object.hasOwn(attributes, name)) {
  82. const value = attributes[name]
  83. if (value === void 0) {
  84. continue
  85. }
  86. if (/^(value|textContent|innerText|innerHTML)$/.test(name)) {
  87. element[name] = value
  88. } else if (name === "style") {
  89. setStyle(element, value, true)
  90. } else if (/on\w+/.test(name)) {
  91. const type = name.slice(2)
  92. addEventListener(element, type, value)
  93. } else {
  94. setAttribute(element, name, value)
  95. }
  96. }
  97. }
  98. }
  99. return element
  100. }
  101. var setStyle = (element, values, overwrite) => {
  102. if (!element) {
  103. return
  104. }
  105. const style = element.style
  106. if (typeof values === "string") {
  107. style.cssText = overwrite ? values : style.cssText + ";" + values
  108. return
  109. }
  110. if (overwrite) {
  111. style.cssText = ""
  112. }
  113. for (const key in values) {
  114. if (Object.hasOwn(values, key)) {
  115. style[key] = values[key].replace("!important", "")
  116. }
  117. }
  118. }
  119. if (typeof Object.hasOwn !== "function") {
  120. Object.hasOwn = (instance, prop) =>
  121. Object.prototype.hasOwnProperty.call(instance, prop)
  122. }
  123. var addElement = (parentNode, tagName, attributes) => {
  124. if (typeof parentNode === "string" || typeof tagName === "string") {
  125. const element = GM_addElement(parentNode, tagName, attributes)
  126. setAttributes(element, attributes)
  127. return element
  128. }
  129. setAttributes(tagName, attributes)
  130. parentNode.append(tagName)
  131. return tagName
  132. }
  133. var addStyle = (styleText) => GM_addStyle(styleText)
  134. var registerMenuCommand = (name, callback, accessKey) =>
  135. window === top && GM_registerMenuCommand(name, callback, accessKey)
  136. var getValue = (key) => {
  137. const value = GM_getValue(key)
  138. return value && value !== "undefined" ? JSON.parse(value) : void 0
  139. }
  140. var setValue = (key, value) => {
  141. if (value !== void 0) GM_setValue(key, JSON.stringify(value))
  142. }
  143. var addValueChangeListener = (key, func) => {
  144. const listenerId = GM_addValueChangeListener(key, func)
  145. return () => {
  146. GM_removeValueChangeListener(listenerId)
  147. }
  148. }
  149. var style_default =
  150. "#browser_extension_settings{--browser-extension-settings-background-color: #f3f3f3;--browser-extension-settings-text-color: #444444;position:fixed;top:10px;right:30px;min-width:250px;max-height:90%;overflow-y:auto;overflow-x:hidden;display:none;box-sizing:border-box;padding:10px 15px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color);z-index:100000;border-radius:5px;-webkit-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);-moz-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);box-shadow:0px 10px 39px 10px rgba(62,66,66,.22) !important}#browser_extension_settings h2{text-align:center;margin:5px 0 0;font-size:18px;font-weight:600;border:none}#browser_extension_settings footer{display:flex;justify-content:center;flex-direction:column;font-size:11px;margin:10px auto 0px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color)}#browser_extension_settings footer a{color:#217dfc;text-decoration:none;padding:0}#browser_extension_settings footer p{text-align:center;padding:0;margin:2px;line-height:13px}#browser_extension_settings .option_groups{background-color:#fff;padding:6px 15px 6px 15px;border-radius:10px;display:flex;flex-direction:column;margin:10px 0 0}#browser_extension_settings .option_groups .action{font-size:14px;border-top:1px solid #ccc;padding:6px 0 6px 0;color:#217dfc;cursor:pointer}#browser_extension_settings .option_groups textarea{margin:10px 0 10px 0;height:100px;width:100%;border:1px solid #a9a9a9;border-radius:4px;box-sizing:border-box}#browser_extension_settings .switch_option{display:flex;justify-content:space-between;align-items:center;border-top:1px solid #ccc;padding:6px 0 6px 0;font-size:14px}#browser_extension_settings .switch_option:first-of-type,#browser_extension_settings .option_groups .action:first-of-type{border-top:none}#browser_extension_settings .switch_option>span{margin-right:10px}#browser_extension_settings .option_groups .tip{position:relative;margin:0;padding:0 15px 0 0;border:none;max-width:none;font-size:14px}#browser_extension_settings .option_groups .tip .tip_anchor{cursor:help;text-decoration:underline}#browser_extension_settings .option_groups .tip .tip_content{position:absolute;bottom:15px;left:0;background-color:#fff;color:var(--browser-extension-settings-text-color);text-align:left;padding:10px;display:none;border-radius:5px;-webkit-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);-moz-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);box-shadow:0px 10px 39px 10px rgba(62,66,66,.22) !important}#browser_extension_settings .option_groups .tip .tip_anchor:hover+.tip_content,#browser_extension_settings .option_groups .tip .tip_content:hover{display:block}#browser_extension_settings .option_groups .tip p,#browser_extension_settings .option_groups .tip pre{margin:revert;padding:revert}#browser_extension_settings .option_groups .tip pre{font-family:Consolas,panic sans,bitstream vera sans mono,Menlo,microsoft yahei,monospace;font-size:13px;letter-spacing:.015em;line-height:120%;white-space:pre;overflow:auto;background-color:#f5f5f5;word-break:normal;overflow-wrap:normal;padding:.5em;border:none}#browser_extension_settings .container{--button-width: 51px;--button-height: 24px;--toggle-diameter: 20px;--color-off: #e9e9eb;--color-on: #34c759;width:var(--button-width);height:var(--button-height);position:relative;padding:0;margin:0;flex:none}#browser_extension_settings input[type=checkbox]{opacity:0;width:0;height:0;position:absolute}#browser_extension_settings .switch{width:100%;height:100%;display:block;background-color:var(--color-off);border-radius:calc(var(--button-height)/2);cursor:pointer;transition:all .2s ease-out}#browser_extension_settings .slider{width:var(--toggle-diameter);height:var(--toggle-diameter);position:absolute;left:2px;top:calc(50% - var(--toggle-diameter)/2);border-radius:50%;background:#fff;box-shadow:0px 3px 8px rgba(0,0,0,.15),0px 3px 1px rgba(0,0,0,.06);transition:all .2s ease-out;cursor:pointer}#browser_extension_settings input[type=checkbox]:checked+.switch{background-color:var(--color-on)}#browser_extension_settings input[type=checkbox]:checked+.switch .slider{left:calc(var(--button-width) - var(--toggle-diameter) - 2px)}"
  151. function createSwitch(options = {}) {
  152. const container = createElement("label", { class: "container" })
  153. const checkbox = createElement(
  154. "input",
  155. options.checked ? { type: "checkbox", checked: "" } : { type: "checkbox" }
  156. )
  157. addElement(container, checkbox)
  158. const switchElm = createElement("span", { class: "switch" })
  159. addElement(switchElm, "span", { class: "slider" })
  160. addElement(container, switchElm)
  161. if (options.onchange) {
  162. addEventListener(checkbox, "change", options.onchange)
  163. }
  164. return container
  165. }
  166. function createSwitchOption(text, options) {
  167. const div = createElement("div", { class: "switch_option" })
  168. addElement(div, "span", { textContent: text })
  169. div.append(createSwitch(options))
  170. return div
  171. }
  172. var settingsElementId =
  173. "browser_extension_settings_" + String(Math.round(Math.random() * 1e4))
  174. var getSettingsElement = () => $("#" + settingsElementId)
  175. var getSettingsStyle = () =>
  176. style_default.replace(/browser_extension_settings/gm, settingsElementId)
  177. var storageKey = "settings"
  178. var settingsOptions = {}
  179. var settingsTable = {}
  180. var settings = {}
  181. async function getSettings() {
  182. var _a
  183. return (_a = await getValue(storageKey)) != null ? _a : {}
  184. }
  185. async function saveSattingsValue(key, value) {
  186. const settings2 = await getSettings()
  187. settings2[key] =
  188. settingsTable[key] && settingsTable[key].defaultValue === value
  189. ? void 0
  190. : value
  191. await setValue(storageKey, settings2)
  192. }
  193. function getSettingsValue(key) {
  194. var _a
  195. return Object.hasOwn(settings, key)
  196. ? settings[key]
  197. : (_a = settingsTable[key]) == null
  198. ? void 0
  199. : _a.defaultValue
  200. }
  201. var modalHandler = (event) => {
  202. let target = event.target
  203. const settingsLayer = getSettingsElement()
  204. if (settingsLayer) {
  205. while (target !== settingsLayer && target) {
  206. target = target.parentNode
  207. }
  208. if (target === settingsLayer) {
  209. return
  210. }
  211. settingsLayer.style.display = "none"
  212. }
  213. removeEventListener(document, "click", modalHandler)
  214. }
  215. async function updateOptions() {
  216. if (!getSettingsElement()) {
  217. return
  218. }
  219. for (const key in settingsTable) {
  220. if (Object.hasOwn(settingsTable, key)) {
  221. const checkbox = $(
  222. `#${settingsElementId} .option_groups .switch_option[data-key="${key}"] input`
  223. )
  224. if (checkbox) {
  225. checkbox.checked = getSettingsValue(key)
  226. }
  227. }
  228. }
  229. const host2 = location.host
  230. const group2 = $(`#${settingsElementId} .option_groups:nth-of-type(2)`)
  231. if (group2) {
  232. group2.style.display = getSettingsValue(
  233. `enableCustomRulesForCurrentSite_${host2}`
  234. )
  235. ? "block"
  236. : "none"
  237. }
  238. const customStyleValue = $(`#${settingsElementId} .option_groups textarea`)
  239. if (customStyleValue) {
  240. customStyleValue.value =
  241. settings[`customRulesForCurrentSite_${host2}`] || ""
  242. }
  243. }
  244. function createSettingsElement() {
  245. let settingsLayer = getSettingsElement()
  246. if (!settingsLayer) {
  247. addStyle(getSettingsStyle())
  248. settingsLayer = addElement(document.body, "div", {
  249. id: settingsElementId,
  250. })
  251. if (settingsOptions.title) {
  252. addElement(settingsLayer, "h2", { textContent: settingsOptions.title })
  253. }
  254. const options = addElement(settingsLayer, "div", {
  255. class: "option_groups",
  256. })
  257. for (const key in settingsTable) {
  258. if (Object.hasOwn(settingsTable, key)) {
  259. const item = settingsTable[key]
  260. if (!item.type || item.type === "switch") {
  261. const switchOption = createSwitchOption(item.title, {
  262. async onchange(event) {
  263. await saveSattingsValue(key, event.target.checked)
  264. },
  265. })
  266. switchOption.dataset.key = key
  267. addElement(options, switchOption)
  268. }
  269. }
  270. }
  271. const options2 = addElement(settingsLayer, "div", {
  272. class: "option_groups",
  273. })
  274. let timeoutId
  275. addElement(options2, "textarea", {
  276. placeholder: `/* Custom rules for internal URLs, matching URLs will be opened in new tabs */`,
  277. onkeyup(event) {
  278. if (timeoutId) {
  279. clearTimeout(timeoutId)
  280. timeoutId = null
  281. }
  282. timeoutId = setTimeout(async () => {
  283. const host2 = location.host
  284. await saveSattingsValue(
  285. `customRulesForCurrentSite_${host2}`,
  286. event.target.value.trim()
  287. )
  288. }, 100)
  289. },
  290. })
  291. const tip = addElement(options2, "div", {
  292. class: "tip",
  293. })
  294. addElement(tip, "a", {
  295. class: "tip_anchor",
  296. textContent: "Examples",
  297. })
  298. const tipContent = addElement(tip, "div", {
  299. class: "tip_content",
  300. innerHTML: `<p>Custom rules for internal URLs, matching URLs will be opened in new tabs</p>
  301. <p>
  302. - One line per url pattern<br>
  303. - All URLs contains '/posts' or '/users/'<br>
  304. <pre>/posts/
  305. /users/</pre>
  306. - Regex is supported<br>
  307. <pre>^/(posts|members)/d+</pre>
  308. - '*' for all URLs
  309. </p>`,
  310. })
  311. if (settingsOptions.footer) {
  312. const footer = addElement(settingsLayer, "footer")
  313. footer.innerHTML =
  314. typeof settingsOptions.footer === "string"
  315. ? settingsOptions.footer
  316. : `<p>Made with \u2764\uFE0F by
  317. <a href="https://www.pipecraft.net/" target="_blank">
  318. Pipecraft
  319. </a></p>`
  320. }
  321. }
  322. return settingsLayer
  323. }
  324. async function showSettings() {
  325. const settingsLayer = createSettingsElement()
  326. await updateOptions()
  327. settingsLayer.style.display = "block"
  328. addEventListener(document, "click", modalHandler)
  329. }
  330. var initSettings = async (options) => {
  331. settingsOptions = options
  332. settingsTable = options.settingsTable || {}
  333. addValueChangeListener(storageKey, async () => {
  334. settings = await getSettings()
  335. await updateOptions()
  336. })
  337. settings = await getSettings()
  338. }
  339. var origin = location.origin
  340. var host = location.host
  341. var config = {
  342. run_at: "document_end",
  343. }
  344. var settingsTable2 = {
  345. enable: {
  346. title: "Enable",
  347. defaultValue: true,
  348. },
  349. [`enableCurrentSite_${host}`]: {
  350. title: "Enable current site",
  351. defaultValue: true,
  352. },
  353. [`enableCustomRulesForCurrentSite_${host}`]: {
  354. title: "Enable custom rules for current site",
  355. defaultValue: false,
  356. },
  357. [`customRulesForCurrentSite_${host}`]: {
  358. title: "Enable custom rules for current site",
  359. defaultValue: "",
  360. type: "textarea",
  361. },
  362. }
  363. function registerMenuCommands() {
  364. registerMenuCommand("\u2699\uFE0F \u8BBE\u7F6E", showSettings, "o")
  365. }
  366. var addAttribute = (element, name, value) => {
  367. const orgValue = getAttribute(element, name)
  368. if (!orgValue) {
  369. setAttribute(element, name, value)
  370. } else if (!orgValue.includes(value)) {
  371. setAttribute(element, name, orgValue + " " + value)
  372. }
  373. }
  374. var shouldOpenInNewTab = (element) => {
  375. var _a
  376. const url = element.href
  377. if (
  378. !url ||
  379. !/^https?:\/\//.test(url) ||
  380. ((_a = element.getAttribute("href")) == null
  381. ? void 0
  382. : _a.startsWith("#"))
  383. ) {
  384. return false
  385. }
  386. if (element.origin !== origin) {
  387. return true
  388. }
  389. if (getSettingsValue(`enableCustomRulesForCurrentSite_${host}`)) {
  390. const rules = (
  391. getSettingsValue(`customRulesForCurrentSite_${host}`) || ""
  392. ).split("\n")
  393. if (rules.includes("*")) {
  394. return true
  395. }
  396. const pathname = element.pathname
  397. for (let rule of rules) {
  398. rule = rule.trim()
  399. if (rule.length === 0) {
  400. continue
  401. }
  402. try {
  403. const regexp = new RegExp(rule)
  404. if (regexp.test(pathname)) {
  405. return true
  406. }
  407. } catch (error) {
  408. console.log(error.message)
  409. if (pathname.includes(rule)) {
  410. return true
  411. }
  412. }
  413. }
  414. }
  415. }
  416. var setAttributeAsOpenInNewTab = (element) => {
  417. if (shouldOpenInNewTab(element)) {
  418. setAttribute(element, "target", "_blank")
  419. addAttribute(element, "rel", "noopener")
  420. }
  421. }
  422. async function main() {
  423. await initSettings({
  424. title: "\u{1F517} Links Helper",
  425. footer: `
  426. <p>After change settings, reload the page to take effect</p>
  427. <p>
  428. <a href="https://github.com/utags/links-helper/issues" target="_blank">
  429. Report and Issue...
  430. </a></p>
  431. <p>Made with \u2764\uFE0F by
  432. <a href="https://www.pipecraft.net/" target="_blank">
  433. Pipecraft
  434. </a></p>`,
  435. settingsTable: settingsTable2,
  436. })
  437. registerMenuCommands()
  438. if (
  439. !getSettingsValue("enable") ||
  440. !getSettingsValue(`enableCurrentSite_${host}`)
  441. ) {
  442. return
  443. }
  444. addEventListener(
  445. document,
  446. "click",
  447. (event) => {
  448. let anchorElement = event.target
  449. while (anchorElement && anchorElement.tagName !== "A") {
  450. anchorElement = anchorElement.parentNode
  451. }
  452. if (anchorElement) {
  453. setAttributeAsOpenInNewTab(anchorElement)
  454. }
  455. },
  456. true
  457. )
  458. for (const element of $$("a")) {
  459. setAttributeAsOpenInNewTab(element)
  460. }
  461. }
  462. main()
  463. })()