diff --git a/app/assets/stylesheets/modules/gem.css b/app/assets/stylesheets/modules/gem.css
index f04b793d00e..cf8c3d54c7a 100644
--- a/app/assets/stylesheets/modules/gem.css
+++ b/app/assets/stylesheets/modules/gem.css
@@ -187,35 +187,6 @@
.gem__code__icon.static {
position: static; }
-.gem__code__tooltip--copy,
-.gem__code__tooltip--copied {
- display: none; }
-
-.clipboard-is-hover,
-.clipboard-is-active {
- display: block;
- position: absolute;
- top: 45px;
- right: 0;
- width: auto;
- padding-left: 10px;
- padding-right: 10px;
- z-index: 1;
- border-radius: 6px;
- background-color: #141c22;
- text-transform: none;
- line-height: 30px;
- text-align: center;
- color: #ffffff; }
- .clipboard-is-hover:before,
- .clipboard-is-active:before {
- content: "";
- position: absolute;
- top: -15px;
- right: 8px;
- border: 8px solid transparent;
- border-bottom: 8px solid #141c22; }
-
.gem__link:before {
margin-right: 16px; }
diff --git a/app/assets/stylesheets/modules/shared.css b/app/assets/stylesheets/modules/shared.css
index 08ece7df3a0..499adb14719 100644
--- a/app/assets/stylesheets/modules/shared.css
+++ b/app/assets/stylesheets/modules/shared.css
@@ -183,6 +183,20 @@ span.github-btn {
margin-top: 5px;
}
+.recovery-code-list {
+ border: none;
+ padding: 10px 20px;
+ font-size: 1.2em;
+ font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ resize: none;
+ background: none;
+}
+
+.recovery-code-list:focus {
+ background: none;
+ outline: none;
+}
+
.recovery-code-list__item {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
diff --git a/app/helpers/rubygems_helper.rb b/app/helpers/rubygems_helper.rb
index 7898a9e9475..d509ae7fb97 100644
--- a/app/helpers/rubygems_helper.rb
+++ b/app/helpers/rubygems_helper.rb
@@ -188,4 +188,33 @@ def github_params(rubygem)
size: "large"
}
end
+
+ def copy_field_tag(name, value)
+ field = text_field_tag(
+ name,
+ value,
+ class: "gem__code",
+ readonly: "readonly",
+ data: { clipboard_target: "source" }
+ )
+
+ button = tag.span(
+ "=",
+ class: "gem__code__icon",
+ title: t("copy_to_clipboard"),
+ data: {
+ action: "click->clipboard#copy",
+ clipboard_target: "button"
+ }
+ )
+
+ tag.div(
+ field + button,
+ class: "gem__code-wrap",
+ data: {
+ controller: "clipboard",
+ clipboard_success_content_value: "✔"
+ }
+ )
+ end
end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 338c6f33691..10a27b831c7 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -7,8 +7,6 @@ LocalTime.start()
import "controllers"
-import "src/clipboard_buttons";
-import "src/multifactor_auths";
import "src/oidc_api_key_role_form";
import "src/pages";
import "src/search";
diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js
index 1213e85c7ac..c3847f16889 100644
--- a/app/javascript/controllers/application.js
+++ b/app/javascript/controllers/application.js
@@ -1,7 +1,11 @@
import { Application } from "@hotwired/stimulus"
+import Clipboard from '@stimulus-components/clipboard'
const application = Application.start()
+// Add vendored controllers
+application.register('clipboard', Clipboard)
+
// Configure Stimulus development experience
application.debug = false
window.Stimulus = application
diff --git a/app/javascript/controllers/recovery_controller.js b/app/javascript/controllers/recovery_controller.js
new file mode 100644
index 00000000000..832bd691bf1
--- /dev/null
+++ b/app/javascript/controllers/recovery_controller.js
@@ -0,0 +1,39 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static values = {
+ confirm: { type: String, default: "Leave without copying recovery codes?" }
+ }
+
+ connect() {
+ this.copied = false;
+ window.addEventListener("beforeunload", this.popUp);
+ }
+
+ popUp(e) {
+ e.preventDefault();
+ e.returnValue = "";
+ }
+
+ copy() {
+ if (!this.copied) {
+ this.copied = true;
+ window.removeEventListener("beforeunload", this.popUp);
+ }
+ }
+
+ submit(e) {
+ e.preventDefault();
+
+ if (!this.element.checkValidity()) {
+ this.element.reportValidity();
+ return;
+ }
+
+ if (this.copied || confirm(this.confirmValue)) {
+ window.removeEventListener("beforeunload", this.popUp);
+ // Don't include the form data in the URL.
+ window.location.href = this.element.action;
+ }
+ }
+}
diff --git a/app/javascript/src/clipboard_buttons.js b/app/javascript/src/clipboard_buttons.js
deleted file mode 100644
index 23dcd50b1fe..00000000000
--- a/app/javascript/src/clipboard_buttons.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import $ from "jquery";
-import ClipboardJS from "clipboard";
-
-$(function() {
- var clipboard = new ClipboardJS('.gem__code__icon');
- var copyTooltip = $('.gem__code__tooltip--copy');
- var copiedTooltip = $('.gem__code__tooltip--copied');
- var copyButtons = $('.gem__code__icon');
-
- function hideCopyShowCopiedTooltips(e) {
- copyTooltip.removeClass("clipboard-is-hover");
- copiedTooltip.insertAfter(e.trigger);
- copiedTooltip.addClass("clipboard-is-active");
- };
-
- clipboard.on('success', function(e) {
- hideCopyShowCopiedTooltips(e);
- e.clearSelection();
- });
-
- clipboard.on('error', function(e) {
- hideCopyShowCopiedTooltips(e);
- copiedTooltip.text("Ctrl-C to Copy");
- });
-
- copyButtons.hover(function() {
- copyTooltip.insertAfter(this);
- copyTooltip.addClass("clipboard-is-hover");
- });
-
- copyButtons.mouseout(function() {
- copyTooltip.removeClass("clipboard-is-hover");
- });
-
- copyButtons.mouseout(function() {
- copiedTooltip.removeClass("clipboard-is-active");
- });
-});
diff --git a/app/javascript/src/multifactor_auths.js b/app/javascript/src/multifactor_auths.js
deleted file mode 100644
index 3802c978f1d..00000000000
--- a/app/javascript/src/multifactor_auths.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import $ from "jquery";
-import ClipboardJS from "clipboard";
-
-function popUp (e) {
- e.preventDefault();
- e.returnValue = "";
-};
-
-function confirmNoRecoveryCopy (e, from) {
- if (from == null){
- e.preventDefault();
- if (confirm("Leave without copying recovery codes?")) {
- window.removeEventListener("beforeunload", popUp);
- $(this).trigger('click', ["non-null"]);
- }
- }
-}
-
-if (document.getElementById("recovery-code-list")) {
- new ClipboardJS(".recovery__copy__icon");
-
- $(".recovery__copy__icon").on("click", function(e){
- $(this).text("[ copied ]");
-
- if( !$(this).is(".clicked") ) {
- e.preventDefault();
- $(this).addClass("clicked");
- window.removeEventListener("beforeunload", popUp);
- $(".form__submit").unbind("click", confirmNoRecoveryCopy);
- }
- });
-
- window.addEventListener("beforeunload", popUp);
- $(".form__submit").on("click", confirmNoRecoveryCopy);
-
- $(".form__checkbox__input").change(function() {
- if(this.checked) {
- $(".form__submit").prop('disabled', false);
- } else {
- $(".form__submit").prop('disabled', true);
- }
- });
-}
diff --git a/app/views/multifactor_auths/recovery.html.erb b/app/views/multifactor_auths/recovery.html.erb
index f1e46bec240..d6cdada9165 100644
--- a/app/views/multifactor_auths/recovery.html.erb
+++ b/app/views/multifactor_auths/recovery.html.erb
@@ -1,18 +1,25 @@
<% @title = t(".title") %>
-
+<%= tag.div(
+ class: "t-body",
+ data: {
+ controller: "clipboard",
+ clipboard_success_content_value: t('copied')
+ }
+) do %>
<%= t ".note_html" %>
-
- <% @mfa_recovery_codes.each do |code| %>
- <%= code %>
- <% end %>
-
+ <%# This tag contains the recovery codes and should not be a part of the form %>
+ <%= text_area_tag "source", @mfa_recovery_codes.join("\n") + "\n", class: "recovery-code-list", rows: @mfa_recovery_codes.size + 1, cols: @mfa_recovery_codes.first.length + 1, readonly: true, data: { clipboard_target: "source" } %>
-
<%= link_to t(".copy"), "#/", class: "t-link--bold recovery__copy__icon", data: { "clipboard-target": "#recovery-code-list" } %>
-
- <%= check_box_tag "checked", "ack", false, class: "form__checkbox__input" %>
- <%= label_tag "checked", t(".saved"), class: "form__checkbox__label" %>
-
- <%= button_to t(".continue"), @continue_path, method: "get", class: "form__submit form__submit--no-hover", disabled: true %>
-
+ <%= form_tag(@continue_path, method: "get", class: "form", data: { controller: "recovery", recovery_confirm_value: t(".confirm_dialog"), action: "recovery#submit" }) do %>
+ <%= link_to t("copy_to_clipboard"), "#/", class: "t-link--bold recovery__copy__icon", data: { action: "clipboard#copy recovery#copy", clipboard_target: "button" } %>
+
+
+ <%= check_box_tag "checked", "ack", false, required: true, class: "form__checkbox__input" %>
+ <%= label_tag "checked", t(".saved"), class: "form__checkbox__label" %>
+
+
+ <%= button_tag t(".continue"), class: "form__submit form__submit--no-hover" %>
+ <% end %>
+<% end %>
diff --git a/app/views/oidc/api_key_roles/github_actions_workflow_view.rb b/app/views/oidc/api_key_roles/github_actions_workflow_view.rb
index a7032b28e00..c250501599c 100644
--- a/app/views/oidc/api_key_roles/github_actions_workflow_view.rb
+++ b/app/views/oidc/api_key_roles/github_actions_workflow_view.rb
@@ -15,7 +15,7 @@ def view_template
return if not_configured
- div(class: "t-body") do
+ div(class: "t-body", data: { controller: "clipboard", clipboard_success_content_value: "✔" }) do
p do
t(".configured_for_html", link_html:
single_gem_role? ? helpers.link_to(gem_name, rubygem_path(gem_name)) : t(".a_gem"))
@@ -30,12 +30,14 @@ def view_template
header(class: "gem__code__header") do
h3(class: "t-list__heading l-mb-0") { code { ".github/workflows/push_gem.yml" } }
- button(class: "gem__code__icon", data: { "clipboard-target": "#workflow_yaml" }) { "=" }
- span(class: "gem__code__tooltip--copy") { t("copy_to_clipboard") }
- span(class: "gem__code__tooltip--copied") { t("copied") }
+ button(
+ class: "gem__code__icon",
+ title: t("copy_to_clipboard"),
+ data: { action: "click->clipboard#copy", clipboard_target: "button" }
+ ) { "=" }
end
pre(class: "gem__code multiline") do
- code(class: "multiline", id: "workflow_yaml") do
+ code(class: "multiline", id: "workflow_yaml", data: { clipboard_target: "source" }) do
plain workflow_yaml
end
end
diff --git a/app/views/rubygems/_gem_members.html.erb b/app/views/rubygems/_gem_members.html.erb
index fc15a017930..9fd19d8b3a4 100644
--- a/app/views/rubygems/_gem_members.html.erb
+++ b/app/views/rubygems/_gem_members.html.erb
@@ -57,12 +57,7 @@
<% if latest_version.sha256.present? %>
<%= t '.sha_256_checksum' %>:
-
-
- =
- <%= t('copy_to_clipboard') %>
- <%= t('copied') %>
-
+ <%= copy_field_tag("sha256", latest_version.sha256_hex) %>
<% end %>
<% if latest_version.cert_chain.present? %>
diff --git a/app/views/rubygems/show.html.erb b/app/views/rubygems/show.html.erb
index 617e95ac320..9cf2931041f 100644
--- a/app/views/rubygems/show.html.erb
+++ b/app/views/rubygems/show.html.erb
@@ -31,19 +31,11 @@
<% else %>
diff --git a/config/importmap.rb b/config/importmap.rb
index d25a50aee3e..4897472eefb 100644
--- a/config/importmap.rb
+++ b/config/importmap.rb
@@ -5,12 +5,12 @@
pin "application"
pin_all_from "app/javascript/src", under: "src"
-pin "clipboard" # @2.0.11
# stimulus.min.js is a compiled asset from stimulus-rails gem
pin "@hotwired/stimulus", to: "stimulus.min.js"
# stimulus-loading.js is a compiled asset only available from stimulus-rails gem
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
+pin "@stimulus-components/clipboard", to: "@stimulus-components--clipboard.js" # @5.0.0
# vendored and adapted from https://github.com/mdo/github-buttons/blob/master/src/js.js
pin "github-buttons"
diff --git a/config/locales/de.yml b/config/locales/de.yml
index fdbace32003..975b1593aa1 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -537,11 +537,10 @@ de:
mfa_recommended_not_yet_enabled:
mfa_recommended_weak_level_enabled:
recovery:
- copied: "[ kopiert ]"
continue: Weiter
title: Wiederherstellungscodes
- copy: "[ kopieren ]"
saved: Ich bestätige, dass ich meine Wiederherstellungscodes gespeichert habe.
+ confirm_dialog:
note_html: Bitte kopieren und speichern
Sie diese Wiederherstellungscodes. Sie können diese Codes verwenden, um sich
anzumelden und Ihre MFA zurückzusetzen, wenn Sie Ihr Authentifizierungsgerät
@@ -896,8 +895,6 @@ de:
continue:
title:
notice_html:
- copied:
- copy:
saved:
webauthn_credential:
confirm_delete:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index b3c9b6c60fd..3a05781c196 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -463,11 +463,10 @@ en:
[WARNING] For protection of your account and gems, we encourage you to change your multi-factor authentication level to 'UI and gem signin' or 'UI and API' at https://rubygems.org/settings/edit.
Your account will be required to have MFA enabled on one of these levels in the future.
recovery:
- copied: "[ copied ]"
continue: Continue
title: Recovery codes
- copy: "[ copy ]"
saved: I acknowledge that I have saved my recovery codes.
+ confirm_dialog: Leave without copying recovery codes?
note_html: "Please copy and save these recovery codes. You can use these codes to login and reset your MFA if your lose your authentication device. Each code can be used once."
already_generated: You should have already saved your recovery codes.
update:
@@ -809,8 +808,6 @@ en:
continue: Continue
title: You have successfully added a security device
notice_html: 'Please copy and paste these recovery codes. You can use these codes to login if you lose your security device. Each code can be used once.'
- copied: "[ copied ]"
- copy: "[ copy ]"
saved: I acknowledge that I have saved my recovery codes.
webauthn_credential:
confirm_delete: "Credential deleted"
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 4c4f5d65958..4efa807b479 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -522,11 +522,10 @@ es:
mfa_recommended_not_yet_enabled:
mfa_recommended_weak_level_enabled:
recovery:
- copied: "[ copiado ]"
continue: Continuar
title: Códigos de recuperación
- copy: "[ copiar ]"
saved: Declaro haber guardado mis códigos de recuperación.
+ confirm_dialog:
note_html: Por favor copia y guarda
estos códigos de recuperación. Puedes usar estos códigos para acceder y restablecer
tu autenticación de múltiples factores si pierdes tu dispositivo. Cada código
@@ -939,8 +938,6 @@ es:
estos códigos de recuperación. Puedes utilizar estos códigos para acceder
si pierdes tu dispositivo de seguridad. Cada código solo se puede usar una
vez.
- copied: "[ copiado ]"
- copy: "[ copiar ]"
saved: Declaro haber guardado mis códigos de recuperación.
webauthn_credential:
confirm_delete: Credencial borrada
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 84a029f80e7..fcd1c729046 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -473,11 +473,10 @@ fr:
mfa_recommended_not_yet_enabled:
mfa_recommended_weak_level_enabled:
recovery:
- copied:
continue: Continuer
title: Codes de récupération
- copy:
saved:
+ confirm_dialog:
note_html:
already_generated:
update:
@@ -846,8 +845,6 @@ fr:
continue:
title:
notice_html:
- copied:
- copy:
saved:
webauthn_credential:
confirm_delete:
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index a2c008f9079..1ea681dfbf4 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -454,11 +454,10 @@ ja:
mfa_recommended_not_yet_enabled:
mfa_recommended_weak_level_enabled:
recovery:
- copied: "[ コピーしました ]"
continue: 続ける
title: 復旧コード
- copy: "[ コピーする ]"
saved: 復旧コードを保存したことを確認しました。
+ confirm_dialog:
note_html: これらの復旧コードをコピー及び保存 してください。認証機器を紛失した場合にこれらのコードを使ってログインしMFAをリセットできます。各コードは1回使えます。
already_generated: 既に復旧コードを保存したはずです。
update:
@@ -808,8 +807,6 @@ ja:
continue: 続ける
title: セキュリティ機器を正常に追加しました。
notice_html: これらの復旧コードをコピー&ペースト してください。セキュリティ機器を紛失した場合にこれらのコードを使ってログインできます。各コードは1度使えます。
- copied: "[ コピーしました ]"
- copy: "[ コピーする ]"
saved: 復旧コードを保存したことを確認しました。
webauthn_credential:
confirm_delete: 認証情報が削除されました
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 2c41e12c4f2..164b20ded42 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -458,11 +458,10 @@ nl:
mfa_recommended_not_yet_enabled:
mfa_recommended_weak_level_enabled:
recovery:
- copied:
continue:
title:
- copy:
saved:
+ confirm_dialog:
note_html:
already_generated:
update:
@@ -801,8 +800,6 @@ nl:
continue:
title:
notice_html:
- copied:
- copy:
saved:
webauthn_credential:
confirm_delete:
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 34a5748e8d5..e99357ef4dc 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -469,11 +469,10 @@ pt-BR:
mfa_recommended_not_yet_enabled:
mfa_recommended_weak_level_enabled:
recovery:
- copied:
continue:
title:
- copy:
saved:
+ confirm_dialog:
note_html:
already_generated:
update:
@@ -824,8 +823,6 @@ pt-BR:
continue:
title:
notice_html:
- copied:
- copy:
saved:
webauthn_credential:
confirm_delete:
diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml
index 7bac93b6af7..6421206df98 100644
--- a/config/locales/zh-CN.yml
+++ b/config/locales/zh-CN.yml
@@ -462,11 +462,10 @@ zh-CN:
mfa_recommended_not_yet_enabled:
mfa_recommended_weak_level_enabled:
recovery:
- copied: "[ 已复制 ]"
continue: 继续
title: 恢复码
- copy: "[ 复制 ]"
saved: 我声明我已经保存了我的恢复码。
+ confirm_dialog:
note_html: 请 复制并保存 这些恢复码。如果您丢失了身份验证设备,您可以使用这些恢复码登录并重置您的多因素验证配置。每个恢复码只能使用一次。
already_generated: 您应该已经保存了您的恢复码。
update:
@@ -815,8 +814,6 @@ zh-CN:
continue: 继续
title: 您已成功添加一个安全设备
notice_html: 请 复制并粘贴 这些恢复码。如果您丢失了安全设备,您可以使用这些恢复码登录。每个恢复码码只能使用一次。
- copied: "[ 已复制 ]"
- copy: "[ 复制 ]"
saved: 我确认我已经保存了我的恢复码。
webauthn_credential:
confirm_delete: 凭证已删除
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index 3dca89fcae8..a2f84c0ded2 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -457,11 +457,10 @@ zh-TW:
mfa_recommended_not_yet_enabled:
mfa_recommended_weak_level_enabled:
recovery:
- copied: "[ 已複製 ]"
continue: 繼續
title: 復原碼
- copy: "[ 複製 ]"
saved:
+ confirm_dialog:
note_html:
already_generated:
update:
@@ -806,8 +805,6 @@ zh-TW:
continue: 繼續
title: 您已成功新增安全裝置
notice_html:
- copied: "[ 已複製 ]"
- copy: "[ 複製 ]"
saved:
webauthn_credential:
confirm_delete: 已刪除認證
diff --git a/test/system/multifactor_auths_test.rb b/test/system/multifactor_auths_test.rb
index 37d82e5a15f..69a06758f53 100644
--- a/test/system/multifactor_auths_test.rb
+++ b/test/system/multifactor_auths_test.rb
@@ -35,7 +35,7 @@ class MultifactorAuthsTest < ApplicationSystemTestCase
register_otp_device
assert page.has_content? "Recovery codes"
- click_link "[ copy ]"
+ click_link "Copy to clipboard"
check "ack"
click_button "Continue"
@@ -174,7 +174,7 @@ def redirect_test_mfa_disabled(path)
register_otp_device
assert page.has_content? "Recovery codes"
- click_link "[ copy ]"
+ click_link "Copy to clipboard"
check "ack"
click_button "Continue"
yield if block_given?
diff --git a/test/system/settings_test.rb b/test/system/settings_test.rb
index b194a91a734..42146f75cd6 100644
--- a/test/system/settings_test.rb
+++ b/test/system/settings_test.rb
@@ -40,7 +40,7 @@ def otp_key
assert page.has_content? "Recovery codes"
- click_link "[ copy ]"
+ click_link "Copy to clipboard"
check "ack"
click_button "Continue"
@@ -103,7 +103,7 @@ def otp_key
recoveries = page.find_by_id("recovery-code-list").text.split
- click_link "[ copy ]"
+ click_link "Copy to clipboard"
check "ack"
click_button "Continue"
page.fill_in "otp", with: recoveries.sample
diff --git a/test/system/webauthn_credentials_test.rb b/test/system/webauthn_credentials_test.rb
index 62a80bf6c60..6930ec829ee 100644
--- a/test/system/webauthn_credentials_test.rb
+++ b/test/system/webauthn_credentials_test.rb
@@ -108,7 +108,7 @@ def sign_in
end
assert_equal recovery_multifactor_auth_path, current_path
- click_on "[ copy ]"
+ click_on "Copy to clipboard"
check "ack"
click_on "Continue"
diff --git a/test/test_helper.rb b/test/test_helper.rb
index db501610808..20e7c01b93c 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -182,7 +182,7 @@ def create_webauthn_credential_while_signed_in
fill_in "Nickname", with: credential_nickname
click_on "Register device"
- click_on "[ copy ]"
+ click_on "Copy to clipboard"
@mfa_recovery_codes = find_all(:css, ".recovery-code-list__item").map(&:text)
check "ack"
click_on "Continue"
diff --git a/vendor/javascript/@stimulus-components--clipboard.js b/vendor/javascript/@stimulus-components--clipboard.js
new file mode 100644
index 00000000000..ffcd8134e42
--- /dev/null
+++ b/vendor/javascript/@stimulus-components--clipboard.js
@@ -0,0 +1,2 @@
+import{Controller as t}from"@hotwired/stimulus";const e=class _Clipboard extends t{connect(){this.hasButtonTarget&&(this.originalContent=this.buttonTarget.innerHTML)}copy(t){t.preventDefault();const e=this.sourceTarget.innerHTML||this.sourceTarget.value;navigator.clipboard.writeText(e).then((()=>this.copied()))}copied(){this.hasButtonTarget&&(this.timeout&&clearTimeout(this.timeout),this.buttonTarget.innerHTML=this.successContentValue,this.timeout=setTimeout((()=>{this.buttonTarget.innerHTML=this.originalContent}),this.successDurationValue))}};e.targets=["button","source"],e.values={successContent:String,successDuration:{type:Number,default:2e3}};let s=e;export{s as default};
+
diff --git a/vendor/javascript/clipboard.js b/vendor/javascript/clipboard.js
deleted file mode 100644
index 6f86b6ef044..00000000000
--- a/vendor/javascript/clipboard.js
+++ /dev/null
@@ -1,186 +0,0 @@
-var t="undefined"!==typeof globalThis?globalThis:"undefined"!==typeof self?self:global;var e={};(function webpackUniversalModuleDefinition(t,n){e=n()})(0,(function(){return function(){var e={686:function(e,n,r){r.d(n,{default:function(){return h}});var o=r(279);var i=r.n(o);var a=r(370);var u=r.n(a);var c=r(817);var l=r.n(c);
-/**
- * Executes a given operation type.
- * @param {String} type
- * @return {Boolean}
- */
-function command(t){try{return document.execCommand(t)}catch(t){return false}}
-/**
- * Cut action wrapper.
- * @param {String|HTMLElement} target
- * @return {String}
- */
-var f=function ClipboardActionCut(t){var e=l()(t);command("cut");return e};var s=f;
-/**
- * Creates a fake textarea element with a value.
- * @param {String} value
- * @return {HTMLElement}
- */
-function createFakeElement(t){var e="rtl"===document.documentElement.getAttribute("dir");var n=document.createElement("textarea");n.style.fontSize="12pt";n.style.border="0";n.style.padding="0";n.style.margin="0";n.style.position="absolute";n.style[e?"right":"left"]="-9999px";var r=window.pageYOffset||document.documentElement.scrollTop;n.style.top="".concat(r,"px");n.setAttribute("readonly","");n.value=t;return n}
-/**
- * Create fake copy action wrapper using a fake element.
- * @param {String} target
- * @param {Object} options
- * @return {String}
- */
-var p=function fakeCopyAction(t,e){var n=createFakeElement(t);e.container.appendChild(n);var r=l()(n);command("copy");n.remove();return r};
-/**
- * Copy action wrapper.
- * @param {String|HTMLElement} target
- * @param {Object} options
- * @return {String}
- */var d=function ClipboardActionCopy(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{container:document.body};var n="";if("string"===typeof t)n=p(t,e);else if(t instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(null===t||void 0===t?void 0:t.type))n=p(t.value,e);else{n=l()(t);command("copy")}return n};var y=d;function _typeof(t){_typeof="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function _typeof(t){return typeof t}:function _typeof(t){return t&&"function"===typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};return _typeof(t)}
-/**
- * Inner function which performs selection from either `text` or `target`
- * properties and then executes copy or cut operations.
- * @param {Object} options
- */var v=function ClipboardActionDefault(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};var e=t.action,n=void 0===e?"copy":e,r=t.container,o=t.target,i=t.text;if("copy"!==n&&"cut"!==n)throw new Error('Invalid "action" value, use either "copy" or "cut"');if(void 0!==o){if(!o||"object"!==_typeof(o)||1!==o.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===n&&o.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===n&&(o.hasAttribute("readonly")||o.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes')}return i?y(i,{container:r}):o?"cut"===n?s(o):y(o,{container:r}):void 0};var b=v;function clipboard_typeof(t){clipboard_typeof="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function _typeof(t){return typeof t}:function _typeof(t){return t&&"function"===typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};return clipboard_typeof(t)}function _classCallCheck(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function _defineProperties(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};(this||t).action="function"===typeof e.action?e.action:(this||t).defaultAction;(this||t).target="function"===typeof e.target?e.target:(this||t).defaultTarget;(this||t).text="function"===typeof e.text?e.text:(this||t).defaultText;(this||t).container="object"===clipboard_typeof(e.container)?e.container:document.body}
-/**
- * Adds a click event listener to the passed trigger.
- * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
- */},{key:"listenClick",value:function listenClick(e){var n=this||t;(this||t).listener=u()(e,"click",(function(t){return n.onClick(t)}))}
-/**
- * Defines a new `ClipboardAction` on each click event.
- * @param {Event} e
- */},{key:"onClick",value:function onClick(e){var n=e.delegateTarget||e.currentTarget;var r=this.action(n)||"copy";var o=b({action:r,container:(this||t).container,target:this.target(n),text:this.text(n)});this.emit(o?"success":"error",{action:r,text:o,trigger:n,clearSelection:function clearSelection(){n&&n.focus();window.getSelection().removeAllRanges()}})}
-/**
- * Default `action` lookup function.
- * @param {Element} trigger
- */},{key:"defaultAction",value:function defaultAction(t){return getAttributeValue("action",t)}
-/**
- * Default `target` lookup function.
- * @param {Element} trigger
- */},{key:"defaultTarget",value:function defaultTarget(t){var e=getAttributeValue("target",t);if(e)return document.querySelector(e)}
-/**
- * Allow fire programmatically a copy action
- * @param {String|HTMLElement} target
- * @param {Object} options
- * @returns Text copied.
- */},{key:"defaultText",
-/**
- * Default `text` lookup function.
- * @param {Element} trigger
- */
-value:function defaultText(t){return getAttributeValue("text",t)}},{key:"destroy",value:function destroy(){(this||t).listener.destroy()}}],[{key:"copy",value:function copy(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{container:document.body};return y(t,e)}
-/**
- * Allow fire programmatically a cut action
- * @param {String|HTMLElement} target
- * @returns Text cutted.
- */},{key:"cut",value:function cut(t){return s(t)}
-/**
- * Returns the support of the given action, or all actions if no action is
- * given.
- * @param {String} [action]
- */},{key:"isSupported",value:function isSupported(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"];var e="string"===typeof t?[t]:t;var n=!!document.queryCommandSupported;e.forEach((function(t){n=n&&!!document.queryCommandSupported(t)}));return n}}]);return Clipboard}(i());var h=_},828:function(t){var e=9;if("undefined"!==typeof Element&&!Element.prototype.matches){var n=Element.prototype;n.matches=n.matchesSelector||n.mozMatchesSelector||n.msMatchesSelector||n.oMatchesSelector||n.webkitMatchesSelector}
-/**
- * Finds the closest parent that matches a selector.
- *
- * @param {Element} element
- * @param {String} selector
- * @return {Function}
- */function closest(t,n){while(t&&t.nodeType!==e){if("function"===typeof t.matches&&t.matches(n))return t;t=t.parentNode}}t.exports=closest},438:function(e,n,r){var o=r(828);
-/**
- * Delegates event to a selector.
- *
- * @param {Element} element
- * @param {String} selector
- * @param {String} type
- * @param {Function} callback
- * @param {Boolean} useCapture
- * @return {Object}
- */function _delegate(e,n,r,o,i){var a=listener.apply(this||t,arguments);e.addEventListener(r,a,i);return{destroy:function(){e.removeEventListener(r,a,i)}}}
-/**
- * Delegates event to a selector.
- *
- * @param {Element|String|Array} [elements]
- * @param {String} selector
- * @param {String} type
- * @param {Function} callback
- * @param {Boolean} useCapture
- * @return {Object}
- */function delegate(t,e,n,r,o){if("function"===typeof t.addEventListener)return _delegate.apply(null,arguments);if("function"===typeof n)return _delegate.bind(null,document).apply(null,arguments);"string"===typeof t&&(t=document.querySelectorAll(t));return Array.prototype.map.call(t,(function(t){return _delegate(t,e,n,r,o)}))}
-/**
- * Finds closest match and invokes callback.
- *
- * @param {Element} element
- * @param {String} selector
- * @param {String} type
- * @param {Function} callback
- * @return {Function}
- */function listener(t,e,n,r){return function(n){n.delegateTarget=o(n.target,e);n.delegateTarget&&r.call(t,n)}}e.exports=delegate},879:function(t,e){
-/**
- * Check if argument is a HTML element.
- *
- * @param {Object} value
- * @return {Boolean}
- */
-e.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType};
-/**
- * Check if argument is a list of HTML elements.
- *
- * @param {Object} value
- * @return {Boolean}
- */e.nodeList=function(t){var n=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===n||"[object HTMLCollection]"===n)&&"length"in t&&(0===t.length||e.node(t[0]))};
-/**
- * Check if argument is a string.
- *
- * @param {Object} value
- * @return {Boolean}
- */e.string=function(t){return"string"===typeof t||t instanceof String};
-/**
- * Check if argument is a function.
- *
- * @param {Object} value
- * @return {Boolean}
- */e.fn=function(t){var e=Object.prototype.toString.call(t);return"[object Function]"===e}},370:function(t,e,n){var r=n(879);var o=n(438);
-/**
- * Validates all params and calls the right
- * listener function based on its target type.
- *
- * @param {String|HTMLElement|HTMLCollection|NodeList} target
- * @param {String} type
- * @param {Function} callback
- * @return {Object}
- */function listen(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!r.string(e))throw new TypeError("Second argument must be a String");if(!r.fn(n))throw new TypeError("Third argument must be a Function");if(r.node(t))return listenNode(t,e,n);if(r.nodeList(t))return listenNodeList(t,e,n);if(r.string(t))return listenSelector(t,e,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}
-/**
- * Adds an event listener to a HTML element
- * and returns a remove listener function.
- *
- * @param {HTMLElement} node
- * @param {String} type
- * @param {Function} callback
- * @return {Object}
- */function listenNode(t,e,n){t.addEventListener(e,n);return{destroy:function(){t.removeEventListener(e,n)}}}
-/**
- * Add an event listener to a list of HTML elements
- * and returns a remove listener function.
- *
- * @param {NodeList|HTMLCollection} nodeList
- * @param {String} type
- * @param {Function} callback
- * @return {Object}
- */function listenNodeList(t,e,n){Array.prototype.forEach.call(t,(function(t){t.addEventListener(e,n)}));return{destroy:function(){Array.prototype.forEach.call(t,(function(t){t.removeEventListener(e,n)}))}}}
-/**
- * Add an event listener to a selector
- * and returns a remove listener function.
- *
- * @param {String} selector
- * @param {String} type
- * @param {Function} callback
- * @return {Object}
- */function listenSelector(t,e,n){return o(document.body,t,e,n)}t.exports=listen},817:function(t){function select(t){var e;if("SELECT"===t.nodeName){t.focus();e=t.value}else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly","");t.select();t.setSelectionRange(0,t.value.length);n||t.removeAttribute("readonly");e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var r=window.getSelection();var o=document.createRange();o.selectNodeContents(t);r.removeAllRanges();r.addRange(o);e=r.toString()}return e}t.exports=select},279:function(e){function E(){}E.prototype={on:function(e,n,r){var o=(this||t).e||((this||t).e={});(o[e]||(o[e]=[])).push({fn:n,ctx:r});return this||t},once:function(e,n,r){var o=this||t;function listener(){o.off(e,listener);n.apply(r,arguments)}listener._=n;return this.on(e,listener,r)},emit:function(e){var n=[].slice.call(arguments,1);var r=(((this||t).e||((this||t).e={}))[e]||[]).slice();var o=0;var i=r.length;for(o;o