From 0d2e9522ffbfc045177b9b21a57c29d907fac635 Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 27 Nov 2025 14:10:55 +0100 Subject: [PATCH] Replace Rails UJS library (#37031) --- app/javascript/mastodon/common.ts | 8 +-- app/javascript/mastodon/utils/links.ts | 88 ++++++++++++++++++++++++++ package.json | 2 - yarn.lock | 16 ----- 4 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 app/javascript/mastodon/utils/links.ts diff --git a/app/javascript/mastodon/common.ts b/app/javascript/mastodon/common.ts index e621a24e39..33d2b5ad17 100644 --- a/app/javascript/mastodon/common.ts +++ b/app/javascript/mastodon/common.ts @@ -1,9 +1,5 @@ -import Rails from '@rails/ujs'; +import { setupLinkListeners } from './utils/links'; export function start() { - try { - Rails.start(); - } catch { - // If called twice - } + setupLinkListeners(); } diff --git a/app/javascript/mastodon/utils/links.ts b/app/javascript/mastodon/utils/links.ts new file mode 100644 index 0000000000..02b74bde4d --- /dev/null +++ b/app/javascript/mastodon/utils/links.ts @@ -0,0 +1,88 @@ +import { on } from 'delegated-events'; + +export function setupLinkListeners() { + on('click', 'a[data-confirm]', handleConfirmLink); + + // We don't want to target links with data-confirm here, as those are handled already. + on('click', 'a[data-method]:not([data-confirm])', handleMethodLink); +} + +function handleConfirmLink(event: MouseEvent) { + const anchor = event.currentTarget; + if (!(anchor instanceof HTMLAnchorElement)) { + return; + } + const message = anchor.dataset.confirm; + if (!message || !window.confirm(message)) { + event.preventDefault(); + return; + } + + if (anchor.dataset.method) { + handleMethodLink(event); + } +} + +function handleMethodLink(event: MouseEvent) { + const anchor = event.currentTarget; + if (!(anchor instanceof HTMLAnchorElement)) { + return; + } + + const method = anchor.dataset.method?.toLowerCase(); + if (!method) { + return; + } + event.preventDefault(); + + // Create and submit a form with the specified method. + const form = document.createElement('form'); + form.method = 'post'; + form.action = anchor.href; + + // Add the hidden _method input to simulate other HTTP methods. + const methodInput = document.createElement('input'); + methodInput.type = 'hidden'; + methodInput.name = '_method'; + methodInput.value = method; + form.appendChild(methodInput); + + // Add CSRF token if available for same-origin requests. + const csrf = getCSRFToken(); + if (csrf && !isCrossDomain(anchor.href)) { + const csrfInput = document.createElement('input'); + csrfInput.type = 'hidden'; + csrfInput.name = csrf.param; + csrfInput.value = csrf.token; + form.appendChild(csrfInput); + } + + // The form needs to be in the document to be submitted. + form.style.display = 'none'; + document.body.appendChild(form); + + // We use requestSubmit to ensure any form submit handlers are properly invoked. + form.requestSubmit(); +} + +function getCSRFToken() { + const param = document.querySelector( + 'meta[name="csrf-param"]', + ); + const token = document.querySelector( + 'meta[name="csrf-token"]', + ); + if (param && token) { + return { param: param.content, token: token.content }; + } + return null; +} + +function isCrossDomain(href: string) { + const link = document.createElement('a'); + link.href = href; + return ( + link.protocol !== window.location.protocol || + link.host !== window.location.host + ); +} diff --git a/package.json b/package.json index e1c91fbda0..64a1a00193 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "@gamestdio/websocket": "^0.3.2", "@github/webauthn-json": "^2.1.1", "@optimize-lodash/rollup-plugin": "^5.0.2", - "@rails/ujs": "7.1.600", "@react-spring/web": "^9.7.5", "@reduxjs/toolkit": "^2.0.1", "@use-gesture/react": "^10.3.1", @@ -149,7 +148,6 @@ "@types/object-assign": "^4.0.30", "@types/prop-types": "^15.7.5", "@types/punycode": "^2.1.0", - "@types/rails__ujs": "^6.0.4", "@types/react": "^18.2.7", "@types/react-dom": "^18.2.4", "@types/react-helmet": "^6.1.6", diff --git a/yarn.lock b/yarn.lock index 38c04d9fd9..7b91fd6ff1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2708,7 +2708,6 @@ __metadata: "@gamestdio/websocket": "npm:^0.3.2" "@github/webauthn-json": "npm:^2.1.1" "@optimize-lodash/rollup-plugin": "npm:^5.0.2" - "@rails/ujs": "npm:7.1.600" "@react-spring/web": "npm:^9.7.5" "@reduxjs/toolkit": "npm:^2.0.1" "@storybook/addon-a11y": "npm:^10.0.6" @@ -2728,7 +2727,6 @@ __metadata: "@types/object-assign": "npm:^4.0.30" "@types/prop-types": "npm:^15.7.5" "@types/punycode": "npm:^2.1.0" - "@types/rails__ujs": "npm:^6.0.4" "@types/react": "npm:^18.2.7" "@types/react-dom": "npm:^18.2.4" "@types/react-helmet": "npm:^6.1.6" @@ -3205,13 +3203,6 @@ __metadata: languageName: node linkType: hard -"@rails/ujs@npm:7.1.600": - version: 7.1.600 - resolution: "@rails/ujs@npm:7.1.600" - checksum: 10c0/0ccaa68a08fbc7b084ab89a1fe49520a5cba6d99f4b0feaf0cb3d00334c59d8d798932d7e49b84aa388875d039ea1e17eb115ed96a80ad157e408a13eceef53e - languageName: node - linkType: hard - "@react-spring/animated@npm:~9.7.5": version: 9.7.5 resolution: "@react-spring/animated@npm:9.7.5" @@ -4297,13 +4288,6 @@ __metadata: languageName: node linkType: hard -"@types/rails__ujs@npm:^6.0.4": - version: 6.0.4 - resolution: "@types/rails__ujs@npm:6.0.4" - checksum: 10c0/7477cb03a0e1339b9cd5c8ac4a197a153e2ff48742b2f527c5a39dcdf80f01493011e368483290d3717662c63066fada3ab203a335804cbb3573cf575f37007e - languageName: node - linkType: hard - "@types/range-parser@npm:*": version: 1.2.7 resolution: "@types/range-parser@npm:1.2.7"